Files
gangyan/langchain-chat/server/chat/tools_v2.py
liuguancen 316def2145 feat(langchain-chat): LangGraph 重写 agent 内核
主要变化:
- 新增 agent_v2.py: 用 LangGraph create_react_agent + astream_events
  替代原 agent_chat_test 的 LLM step-routing 死循环
- 新增 tools_v2.py: 闭包工厂模式,每个请求按 uuid 生成工具列表,
  消除 toolinput 字符串拼 JSON 注入 uuid 的旧 hack
- chat_test.py:266-346: 删 11 次 count_process 重试外层和事件
  分发 spaghetti,换成 agent_run 单次调用 + 简单事件 dispatcher
- policy_fun_iast.py:168-187: 修 broken <think> filter
  老代码把 start_flag 设反了(看见 <think> 才开始 yield)导致
  非 think 模型 yield 不出任何内容;改为正确跳过 <think>...</think> 块

模型函数调用通过 langchain_openai.ChatOpenAI(不能用旧版
langchain_community.chat_models.ChatOpenAI,没有现代 tool calling)。
依赖: langgraph==0.0.49 + langchain-core==0.1.53(已在服务器装好)。

非 stream 分支保留旧 agent_chat_test 路径(极少触发,回归风险低)。
旧版回滚: git checkout backup/pre-langgraph

实测对比:
- 旧版 30-60s,答案 0 字(filter 卡死后展示 11 次重试)
- 新版 25-40s,答案完整(含工具调用、参考文献、推荐问题、摘要)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:20:00 +08:00

143 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
LangGraph 版工具集:闭包工厂注入 uuid统一异常包装。
为什么要重写:
1. 旧版 tools 用 `query` 字符串里塞 JSON + uuid 的 hack 传 metadata
2. 旧版 LLM 工具调度靠多次 LLM 路由,慢且容易循环
3. 这里给每个工具暴露结构化 args_schema交给 LangGraph ReAct 直接 function-calling
"""
import json
import logging
from typing import List, Optional
from langchain_core.tools import tool
# 旧版工具函数仍然复用——只改包装层
from server.agent.tools.search_tool import search_tool as _legacy_kb_search
from server.agent.tools.knowledgebase_kgo_search import knowledgebase_kgo_search as _legacy_kgo_search
from server.agent.tools.draw_plot import create_and_save_plot as _legacy_draw_plot
from server.agent.tools.math import math_count as _legacy_math, code_count as _legacy_code
from server.agent.tools.weather_check import weathercheck as _legacy_weather
from server.agent.tools.search_picture import search_pic as _legacy_search_pic
from server.agent.tools.get_statistical_data import mysql_statistic as _legacy_mysql
logger = logging.getLogger(__name__)
def _safe_call(name: str, fn, *args, **kwargs) -> str:
"""统一异常包装:把 raise 转成给模型的字符串提示,让 ReAct 可恢复。"""
try:
result = fn(*args, **kwargs)
return result if isinstance(result, str) else json.dumps(result, ensure_ascii=False)
except Exception as e:
logger.exception(f"工具 {name} 调用异常")
return f"[工具 {name} 调用异常] {type(e).__name__}: {str(e)[:200]}。请使用其他工具或基于已有信息回答。"
def make_tools(uuid: str) -> list:
"""根据本次请求的 uuid 生成一组闭包工具。
每个工具内部用闭包捕获 uuid调用旧版 func 时按旧 hack 拼装入参字符串。
模型看到的工具入参是结构化的,看不到 uuid。
"""
@tool("知识库联想")
def kb_search(
query: str,
knowledge_name: List[str],
keywords: Optional[List[str]] = None,
) -> str:
"""从指定知识库检索资料。
knowledge_name 必须从如下列表中选择(可多选):
【中国钢铁行业动态库、政策库、期刊论文库、冶金新闻库、冶金中文期刊库、
冶金外文期刊库、冶金OA期刊库、冶金行业新闻库、冶金专业知识库、
冶金行业报告库、报告库、美术专业知识库】。
涉及国家政策时优先选政策库;钢铁行业问题优先选中国钢铁行业动态库。
keywords 是相关关键词2-4 个为宜。
"""
payload = json.dumps({
"query": query,
"knowledge_name": knowledge_name,
"keywords": keywords or [],
}, ensure_ascii=False)
legacy_input = payload + json.dumps({"uuid": uuid}, ensure_ascii=False)
return _safe_call("知识库联想", _legacy_kb_search, legacy_input)
@tool("联网思索")
def web_search(query: str) -> str:
"""联网搜索(智谱 search。query 必须是用户原文,禁止改写。"""
payload = json.dumps({"query": query}, ensure_ascii=False)
legacy_input = payload + json.dumps({"uuid": uuid}, ensure_ascii=False)
return _safe_call("联网思索", _legacy_kgo_search, legacy_input)
@tool("图表绘制")
def draw_plot(
data: dict,
title: str,
xlabel: str,
ylabel: str,
plot_type: str,
) -> str:
"""绘制图表。
data 形如 {"分类A": 23, "分类B": 17}xlabel/ylabel 描述坐标轴含义。
plot_type 必须是 bar / pie / line 之一。
本工具一次只能画一张图;输出图片链接后必须按工具说明输出 markdown 引用。
"""
payload = json.dumps({
"data": data,
"title": title,
"xlabel": xlabel,
"ylabel": ylabel,
"plot_type": plot_type,
}, ensure_ascii=False)
# 旧版 draw_plot 接受 <param>...</param> 包裹的 JSON
wrapped = f"<param>{payload}</param>{json.dumps({'uuid': uuid})}"
return _safe_call("图表绘制", _legacy_draw_plot, wrapped)
@tool("数学运算")
def math_solve(query: str) -> str:
"""数学问题求解。query 描述要求解的数学问题。"""
payload = json.dumps({"query": query}, ensure_ascii=False)
legacy_input = payload + json.dumps({"uuid": uuid}, ensure_ascii=False)
return _safe_call("数学运算", _legacy_math, legacy_input)
@tool("代码专家")
def code_solve(query: str) -> str:
"""代码相关问题,包括写代码、解释代码、调试。"""
payload = json.dumps({"query": query}, ensure_ascii=False)
legacy_input = payload + json.dumps({"uuid": uuid}, ensure_ascii=False)
return _safe_call("代码专家", _legacy_code, legacy_input)
@tool("天气工具")
def weather(location: str) -> str:
"""查询某城市三天内天气。location 是中文城市名,如"北京""""
payload = json.dumps({"location": location}, ensure_ascii=False)
legacy_input = payload + json.dumps({"uuid": uuid}, ensure_ascii=False)
return _safe_call("天气工具", _legacy_weather, legacy_input)
@tool("美术作品获取")
def art_search(query: str) -> str:
"""查询美术作品图片。query 是作品类型描述(如"山水画""草原"),不要传"美术作品"等通用词。"""
payload = json.dumps({"query": query}, ensure_ascii=False)
legacy_input = payload + json.dumps({"uuid": uuid}, ensure_ascii=False)
return _safe_call("美术作品获取", _legacy_search_pic, legacy_input)
@tool("统计数据查询")
def stat_query(query: str) -> str:
"""统计数据库查询。仅有 199x-2023 数据。query 是详细的查询问题描述。"""
payload = json.dumps({"query": query}, ensure_ascii=False)
wrapped = f"<param>{payload}</param>{json.dumps({'uuid': uuid})}"
return _safe_call("统计数据查询", _legacy_mysql, wrapped)
return [
kb_search,
web_search,
draw_plot,
math_solve,
code_solve,
weather,
art_search,
stat_query,
]