展开本页目录
开始前准备什么
- 安装 langgraph、langchain-openai 与 langchain,记录各包版本。
- 公开目录的 glm-5.3 带 Chat 和工具标签,仍需按自己的 token 分组完成工具回合。
- 将下载脚本放在隔离目录;只读固定数据,无数据库、消息或外部写入。
- 配置 Chat 模型
- 绑定只读工具
- 建立回路
- 检查执行证据
- 限制扩展规模
2026-10-23 核验材料;实际执行范围、结果与限制见下方记录。
补充:原有练习资料与下载
只读固定库存,检查调用记录并限制循环。
LangGraph:真实工具消息与回环验证
用17+25库存例子检查真实模型tool_calls、ToolMessage及终止条件。
· 真实LangGraph1.2.16 StateGraph执行LLM→tools→LLM,模型产生add_items(17,25),实际ToolMessage为42,随后最终回答,退出0;不是用预制AIMessage或FakeChatModel替代推理。 网关分支未执行:独立真实探针确认当前测试Key的Gemini组429后停止新增请求,没有伪造网关tool_call。
已取得下列真实结果;仍有内容质量或功能范围需要按本页检查项复核。
- 运行环境
- langgraph1.2.16/langchain-openai1.7.2;独立练习目录;原创虚构输入;本页未使用apizt测试Key
1. 定义并绑定计数工具
定义add_items蓝色/红色整数工具并bind_tools。
2. 保留真实模型消息
StateGraph保留add_messages;LLM节点返回真实AIMessage。
3. 校验工具参数并执行
只有tool_calls存在才进入工具节点,按工具名白名单执行真实函数,ToolMessage携带同一tool_call_id。
4. 携带ToolMessage回环
工具结果返回LLM节点;recursion_limit=6限制循环。
5. 核对42及终止条件
对17+25输入保存所有消息,必须出现真实ToolMessage 42才算工具执行通过。
6. 保存本次原始结果
网关分支必须重新用有效账户、准确模型ID实跑两轮工具请求才能验证。本次当前测试Key的Gemini分组独立探针429,已停止新请求,本地工具循环成功不能证明公网工具协议可用。
这次实际得到什么
- 真实LangGraph1.2.16 StateGraph执行LLM→tools→LLM,模型产生add_items(17,25),实际ToolMessage为42,随后最终回答,退出0;不是用预制AIMessage或FakeChatModel替代推理。 网关分支未执行:独立真实探针确认当前测试Key的Gemini组429后停止新增请求,没有伪造网关tool_call。
- 模型为本次隔离Ollama原生导入的明确别名tutorial-qwen3-proof;权重和原始请求/响应留档。
- 网关分支未发起:当前测试Key的Gemini组独立探针429后停止新增请求。
使用这份结果时注意
- 只证明对应SDK和本地模型的本题任务,不证明apizt公网账户、Claude或所有模型均可用。
- 本次小样例通过不能推断复杂工具、多轮高风险任务也可靠。
- 仅本地工具回路有真实成功,未验证公网工具协议,不伪造tool_call或网关成功。
四步闭环,每一步都有可检查的对象
| 步骤 | 对象 | 通过条件 |
|---|---|---|
| 模型决策 | AIMessage.tool_calls | 有 name、args、id,名称是 lookup_stock |
| 程序查询 | lookup_stock.invoke(args) | 固定表中读取 DEMO-RED=7、DEMO-BLUE=0 |
| 返回结果 | ToolMessage.tool_call_id | 与该次调用 id 一致,保留每个工具结果 |
| 最终回答 | 没有剩余 tools 的 AIMessage | 库存与工具一致,不编造未知 SKU |
- 步骤
- 模型决策
- 对象
- AIMessage.tool_calls
- 通过条件
- 有 name、args、id,名称是 lookup_stock
- 步骤
- 程序查询
- 对象
- lookup_stock.invoke(args)
- 通过条件
- 固定表中读取 DEMO-RED=7、DEMO-BLUE=0
- 步骤
- 返回结果
- 对象
- ToolMessage.tool_call_id
- 通过条件
- 与该次调用 id 一致,保留每个工具结果
- 步骤
- 最终回答
- 对象
- 没有剩余 tools 的 AIMessage
- 通过条件
- 库存与工具一致,不编造未知 SKU
可复制实现:只读库存工具与循环上限
脚本明确 use_responses_api=False,从 Chat 兼容协议开始。LangGraph 的 messages 状态保留工具来回;recursion_limit=8 限制异常循环。运行将产生在线调用费用,本页没有执行真实模型请求。
langgraph-stock.py
import json
import os
from typing import Literal
from langchain.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import END, START, MessagesState, StateGraph
CALLS: list[dict[str, object]] = []
@tool
def lookup_stock(sku: str) -> dict[str, object]:
"""Read the fixed demo stock for one SKU. This tool cannot change inventory."""
result: dict[str, object] = {"sku": sku, "stock": {"DEMO-RED": 7, "DEMO-BLUE": 0}.get(sku), "source": "local-fixture"}
CALLS.append(result)
return result
llm = ChatOpenAI(
model=os.environ.get("tokenba_MODEL", "glm-5.3"),
api_key=os.environ["tokenba_API_KEY"],
base_url="https://api.apizt.com/v1",
use_responses_api=False,
max_tokens=4096,
max_retries=0,
timeout=90,
).bind_tools([lookup_stock])
def model_node(state: MessagesState) -> dict[str, list[AIMessage]]:
answer = llm.invoke([SystemMessage(content="必须先用 lookup_stock 查询两个 SKU,再根据工具结果回答库存。工具返回 null 时说明未知,不能编造。")]+state["messages"])
print("usage:", answer.usage_metadata)
return {"messages": [answer]}
def tool_node(state: MessagesState) -> dict[str, list[ToolMessage]]:
last = state["messages"][-1]
if not isinstance(last, AIMessage):
raise RuntimeError("Expected AIMessage")
replies: list[ToolMessage] = []
for call in last.tool_calls:
if call["name"] != "lookup_stock":
raise RuntimeError("Unexpected tool name")
result = lookup_stock.invoke(call["args"])
replies.append(ToolMessage(content=json.dumps(result, ensure_ascii=False), tool_call_id=call["id"]))
return {"messages": replies}
def route(state: MessagesState) -> Literal["tools", "__end__"]:
last = state["messages"][-1]
return "tools" if isinstance(last, AIMessage) and last.tool_calls else END
builder = StateGraph(MessagesState)
builder.add_node("model", model_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "model")
builder.add_conditional_edges("model", route, ["tools", END])
builder.add_edge("tools", "model")
graph = builder.compile()
result = graph.invoke({"messages": [HumanMessage(content="查询 DEMO-RED 和 DEMO-BLUE 的库存,分别告诉我能否购买。只查库存。") ]}, config={"recursion_limit": 8})
found = {str(row["sku"]): row["stock"] for row in CALLS}
if found.get("DEMO-RED") != 7 or found.get("DEMO-BLUE") != 0:
raise RuntimeError("Missing tool calls: a plausible text answer is not a pass")
print("tool_calls:", json.dumps(CALLS, ensure_ascii=False))
print("answer:", result["messages"][-1].content)小任务验收:有查询记录,再判断回答
- 先运行 pip install langgraph langchain-openai langchain,再在实际 Python 启动环境提供 tokenba_API_KEY。
- 运行 python3 langgraph-stock.py。脚本检查两个 SKU 真的出现于工具记录;漏调用直接抛错。
- 最终回答应说明红色样品 7 件可买、蓝色 0 件不可买;人工比对原固定表。
- 每轮输出 usage_metadata;保存所有回合、错误和实际账单。时间与费用未测时留空。
错误要按回路阶段处理
| 现象 | 先看 | 处理 |
|---|---|---|
| 只回答库存,无工具调用 | AIMessage.tool_calls 和指令 | 确认模型工具支持;不把猜中当通过 |
| tool_call_id 缺失或不一致 | 原 AIMessage 与对应 ToolMessage | 保持调用关联,不能换一个随机 id |
| 循环直到上限 | tools 返回和路由条件 | 保留错误,检查空结果、重复调用与终止条件 |
| 新模型 400 | 实际 tools schema 与参数 | 先移除非必需专有参数,保留业务验收 |
| JSON 字段丢失 | ChatOpenAI 对非标准字段的支持 | 核对适配器;不能假定厂商额外字段被保留 |
- 现象
- 只回答库存,无工具调用
- 先看
- AIMessage.tool_calls 和指令
- 处理
- 确认模型工具支持;不把猜中当通过
- 现象
- tool_call_id 缺失或不一致
- 先看
- 原 AIMessage 与对应 ToolMessage
- 处理
- 保持调用关联,不能换一个随机 id
- 现象
- 循环直到上限
- 先看
- tools 返回和路由条件
- 处理
- 保留错误,检查空结果、重复调用与终止条件
- 现象
- 新模型 400
- 先看
- 实际 tools schema 与参数
- 处理
- 先移除非必需专有参数,保留业务验收
- 现象
- JSON 字段丢失
- 先看
- ChatOpenAI 对非标准字段的支持
- 处理
- 核对适配器;不能假定厂商额外字段被保留
从只读开始,再加入有副作用的工具
这份工具只读本地固定表,没有支付、下单、邮件或数据库写入。扩展时让服务端校验参数、限制工具权限和做业务幂等;模型生成合法参数并不等于用户授权了操作。
本页验证设计使用 Chat function tools。Responses 内置搜索、异步工具、原生 Gemini parts、图像和 MCP 是不同契约,要分别验收。
本次补测:先完成搜索要解决的问题,再进入下一步
图节点和消息格式正确之外,工具还必须接收不可被模型改写的权限上下文。
新增权限小样采用 Python 本地查询,不声称已在 LangGraph 新图中执行。
- 原流程继续核对 ToolMessage 的 tool_call_id 与循环上限。
- 查询前固定当前客户;模型选中的其他客户订单必须被工具拒绝。
- 同时保留本客户可读的正例和跨客户拒绝的负例,检查真实返回数据。
新增补测结果;与原有原生工具记录分别保存
{
"status": "denied",
"order": null
}怎样确认已经完成
- 两次真实 lookup_stock 调用和对应工具结果已保存。
- 最终文本与 7 / 0 一致,未知 SKU 不编造。
- 循环受限;所有轮次、usage 与账单分别记录。
常见问题
LangGraph 会自动执行模型说的工具吗?
只有你定义、连接并运行工具节点才会执行。模型返回 tool_calls 只是调用请求,程序要检查名称和参数,并回传相同调用 id。
只改模型 ID 就能切 Responses 吗?
不能。Chat、Responses 与原生厂商协议的工具、状态和返回结构不同,先选择明确协议再验证完整回路。
工具调用了一次就可以接订单系统吗?
还要补业务权限、参数校验、幂等、失败补偿和审批条件。本页只提供只读演示。

