主要变化: - 新增 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>
143 lines
6.2 KiB
Python
143 lines
6.2 KiB
Python
"""
|
||
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,
|
||
]
|