事情本身不复杂,线索都在日志里。langchain-core 0.3 用 @tool 装饰的函数返回 dict,ToolNode 回填后模型看到的是花括号加单引号的字符串,下一轮解析报 JSONDecodeError,改成直接返回字符串就正常。

第一反应是「模型默认按 JSON 解析工具结果」,验证下来不成立。

这类报错背后常见的原因有几处,逐个排除就行。工具结果被 str 序列化后塞进 content。另一种情况是ToolMessage 的 content 只接受字符串或内容块。

不容易想到的是,return_direct 配置让结果直接进最终输出。

动手时先打印 ToolMessage 的 content 类型和内容

python -c "print(type(msg.content), repr(msg.content)[:200])"

随后在工具里先 json.dumps 再返回

python -c "import json;print(json.dumps({'code':0}, ensure_ascii=False))"

或者直接构造 ToolMessage 返回内容块

python -c "print('return ToolMessage(content=blocks, tool_call_id=id)')"

末了用一次两轮调用验证模型能解析

python smoke_tool.py --rounds 2 --tool get_order

怎么算真的好了,看的是同一批现象。第二轮模型能正确读到字段值,日志里 content 是合法 JSON 字符串,不再出现 JSONDecodeError。

容易忽略的是,Python 的 dict 转字符串用的是单引号,不是 JSON,模型和下游解析器都会误判,工具边界上统一用 json.dumps 是最省事的做法。线上能稳定复现的问题都不算难,难的是只在大流量下才出现的那种。

作者 9823189