处理工具调用格式的补丁

This commit is contained in:
minorQAQ committed 2026-06-03 17:58:57 +08:00
1 parent 1690fead4e
commit 5e5a4b7376
7 files changed
+138 -7

No files matched your search

Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -47,9 +47,9 @@ def call_model(state: AgentState) -> dict:
# ----- 打印工具调用信息 -----
if hasattr(response, 'tool_calls') and response.tool_calls:
for tc in response.tool_calls:
tool_name = tc.get('name', 'unknown')
args = tc.get('args', {})
# LangChain 中 tool_call 的 args 可能是 JSON 字符串,尝试解析
# tc 是 ToolCall 对象,可直接访问 .name 和 .args
tool_name = tc.get('name') if isinstance(tc, dict) else tc.name
args = tc.get('args') if isinstance(tc, dict) else tc.args
if isinstance(args, str):
try:
args = json.loads(args)
Binary file not shown.
@@ -0,0 +1,39 @@
{
"version": 2,
"turns": [
{
"user": {
"content": [
{
"type": "text",
"text": "帮我打开b站"
}
]
},
"assistant": {
"text": "<tool_call>\n<function=browser_control>\n<parameter=action>\nopen\n</parameter>\n<parameter=url>\nhttps://www.bilibili.com\n</parameter>\n</function>\n</tool_call>",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null
}
},
{
"user": {
"content": [
{
"type": "text",
"text": "现在几点了"
}
]
},
"assistant": {
"text": "<tool_call>\n<function=date_query_class>\n</function>\n</tool_call>",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null
}
}
]
}
@@ -0,0 +1,39 @@
{
"version": 2,
"turns": [
{
"user": {
"content": [
{
"type": "text",
"text": "现在几点了"
}
]
},
"assistant": {
"text": "现在是 2026年6月3日 17:56:36。",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null
}
},
{
"user": {
"content": [
{
"type": "text",
"text": "帮我打开b站和虎牙"
}
]
},
"assistant": {
"text": "好的!从截图中可以看到:\n\n1. **B站已经打开了**:第5个标签页显示的是\"啊哈啊哈! - bilibili\"\n2. **虎牙也打开了**:当前页面正在浏览虎牙直播网站,可以看到虎牙的Logo、导航栏(首页、直播、分类、视频、游戏等),以及正在播放的直播内容和右侧的推荐直播间\n\n两个网站都已经成功打开。当前显示的是虎牙直播页面,如果您想查看B站,可以点击第5个标签页切换到B站。",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null
}
}
]
}
+57 -4
View File
@@ -12,7 +12,7 @@ from datetime import datetime as dt
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage, SystemMessage
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage, SystemMessage, ToolCall
from langchain_core.outputs import ChatResult, ChatGeneration
from langchain_openai import ChatOpenAI
@@ -329,6 +329,19 @@ class ChatQwen(ChatOpenAI):
return result
def _create_chat_result(self, response, generation_info=None) -> ChatResult:
# ---------- 打印原始 tool_calls ----------
try:
raw = response if isinstance(response, dict) else response.model_dump()
print("[RAW_RESPONSE]", json.dumps(raw, indent=2, ensure_ascii=False))
choices = raw.get("choices", [])
if choices:
msg = choices[0].get("message", {})
tool_calls = msg.get("tool_calls")
if tool_calls:
print("[RAW_TOOLCALLS]", json.dumps(tool_calls, indent=2, ensure_ascii=False))
except Exception:
pass
result = super()._create_chat_result(response, generation_info)
try:
dumped = response if isinstance(response, dict) else response.model_dump(exclude={"choices": {"__all__": {"message": {"parsed"}}}})
@@ -338,6 +351,7 @@ class ChatQwen(ChatOpenAI):
lc_msg = result.generations[i].message
msg = choice.get("message") or {}
ak = lc_msg.additional_kwargs
# 优先处理 reasoning 内容
merged = ""
for key in ("reasoning_content", "reasoning_details", "reasoning"):
merged = assistant_text(msg.get(key))
@@ -350,27 +364,66 @@ class ChatQwen(ChatOpenAI):
lc_msg.content = merged
elif raw_content:
lc_msg.content = raw_content
# print(f"raw_content={raw_content!r}, merged={merged!r}, final={lc_msg.content!r}")
content_text = lc_msg.content if isinstance(lc_msg.content, str) else ""
# 匹配 <tool_call>...</tool_call> 块
pattern = re.compile(r'<tool_call>\s*(.*?)\s*</tool_call>', re.DOTALL)
matches = pattern.findall(content_text)
if matches:
tool_calls = []
for idx, match in enumerate(matches):
# 先尝试 JSON 解析(兼容旧格式)
try:
call_data = json.loads(match)
tool_calls.append({
"id": f"call_{idx}",
"type": "function",
"function": {
"name": call_data.get("name", ""),
"arguments": json.dumps(call_data.get("arguments", {}))
}
})
continue # 解析成功,跳过 XML 尝试
except json.JSONDecodeError:
pass
# 再尝试 XML 解析
func_match = re.search(r'<function=(\w+)>', match)
if not func_match:
continue
func_name = func_match.group(1)
params = {}
for param_match in re.finditer(r'<parameter=(\w+)>\s*(.*?)\s*</parameter>', match, re.DOTALL):
key = param_match.group(1)
value = param_match.group(2).strip()
# 可选:智能类型转换,先保持简单
params[key] = value
tool_calls.append({
"id": f"call_{idx}",
"type": "function",
"function": {
"name": call_data.get("name", ""),
"arguments": json.dumps(call_data.get("arguments", {}))
"name": func_name,
"arguments": json.dumps(params)
}
})
if tool_calls:
ak["tool_calls"] = tool_calls
# 构造 LangChain 的 ToolCall 对象列表
lc_tool_calls = []
for idx, tc in enumerate(tool_calls):
lc_tool_calls.append(
ToolCall(
id=tc["id"],
name=tc["function"]["name"],
args=json.loads(tc["function"]["arguments"])
)
)
lc_msg.tool_calls = lc_tool_calls
# 清理内容中的 XML/JSON 工具调用痕迹
cleaned = pattern.sub("", content_text).strip()
lc_msg.content = cleaned if cleaned else ""
except Exception: