决策不是执行:手写有限工具循环
用户问“杭州 20 摄氏度相当于多少华氏度”,模型可以提出调用 convert,但它既没有执行 Python,也没有证明结果。执行器收到工具名、参数和调用 ID 后,校验、运行并回填结果;下一次模型请求才看得到这个观察。Chatbot 只需生成文字,Agent 还要在观察后决定是否继续。
第一个可运行循环
保存为 lab.py,用 Python 3.12 运行,无第三方依赖或 API key。decide 是固定输出的决策替身,目的是把协议与执行边界暴露出来;真实模型接入见模型 API 课程,不能用本例评价模型能力。
python
def decide(messages):
if messages[-1]["role"] == "tool":
return {"type": "final", "text": str(messages[-1]["content"])}
return {"type": "call", "id": "c1", "name": "convert", "args": {"c": 20}}
def execute(call):
if call["name"] != "convert":
raise ValueError("unknown tool")
args = call["args"]
if set(args) != {"c"} or type(args["c"]) not in (int, float):
raise ValueError("invalid arguments")
if not -273.15 <= args["c"] <= 1000:
raise ValueError("out of range")
return {"fahrenheit": args["c"] * 9 / 5 + 32}
def run(policy, limit=3):
history = [{"role": "user", "content": "转换 20 摄氏度"}]
for _ in range(limit):
action = policy(history)
if action["type"] == "final":
return {"status": "completed", "history": history, "answer": action["text"]}
history.append({"role": "assistant", "call": action})
try:
result = {"ok": True, "data": execute(action)}
except ValueError as error:
result = {"ok": False, "error": str(error)}
history.append({"role": "tool", "call_id": action["id"], "content": result})
return {"status": "budget_exhausted", "history": history}
result = run(decide)
assert result["status"] == "completed"
assert result["history"][-1]["content"]["data"]["fahrenheit"] == 68
print(result["status"], len(result["history"]))输出 completed 3。第一轮只有用户消息,产生调用意图;第二条消息保存意图,第三条按 call_id 保存观察;第二轮才能结束。若省略 assistant 调用而只加入工具结果,很多真实模型协议无法把结果对应回请求。多个工具结果按 ID 配对,不能按返回先后配对。
错误也是观察,但不是成功
校验错误回填 ok=False,让模型可以修正参数;认证失败则通常要结束或请求重新登录,不应让模型不断猜凭证。模型无工具调用可能是拒答或信息不足,不等于业务完成,所以产品还应将模型结束与业务验收分开。本例仅证明循环协议。
固定 pipeline 适合“读取→转换→返回”这种已知步骤,根本不必调用模型;开放任务才需要动态决策。工具调用次数限制保证最多执行三次,不限制单次工具阻塞时间,生产还需超时、总 deadline 和取消。写工具需要服务端权限与幂等,不能直接复用这个只读示例。
练习
追加一个始终请求 convert 的决策函数,验证 limit=2 时停在 budget_exhausted,历史中恰好两次工具观察;再给 c 传入 True,确认不会作为数值通过。