1. 从一次“翻车”的订单查询说起Function Call 多轮对话指的是 Agent 在完成一个任务时需要多次调用外部函数、并可能在中间向用户追问缺失信息从而形成“用户 ↔ Agent ↔ 工具”交替推进的交互链路。它和普通聊天最大的区别是普通对话靠上下文记忆Function Call 对话靠“状态机 工具链”。适合谁适合正在用 Python 写 Agent、被tool_calls回填、roletool消息顺序、多轮循环退出条件折磨的开发者。我见过太多人第一次写 Function Call代码长这样用户问“我的订单到哪了”模型返回一个tool_calls程序执行函数拿到结果然后……直接把结果print出来当回复。第一轮看起来没问题可一旦用户追问“那什么时候到”模型完全不知道上一轮查过什么因为它根本没看到工具返回的内容。问题不在模型在于消息数组没有被正确“喂”回去。这篇就聚焦一件事把 OpenAI 返回tool_calls→ 本地执行函数 → 回填roletool消息 → 再次请求模型这条链路逐段拆开讲清楚。最后给一份可复制的 Python 骨架包含 tools 定义、循环控制、异常兜底并用 TaoToken 统一 Key 和 API 通道把它跑通。读完你应该能独立复现一个可多轮调用工具的 Agent 最小闭环。2. 前置准备用 TaoToken 统一 Key 与 API 通道在写代码之前先把“通道”这件事解决掉。很多教程默认你已经有某个厂商的 Key但实际开发里经常要在不同模型之间切换对比每个厂商一套 Key、一套 base_url、一套 SDK 参数改起来很烦。TaoToken 的思路是给你一个统一的 API 入口OpenAI 兼容格式换模型基本只改model字段。你需要做两件事拿到 Key确认 base_url。第一步打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。第二步进入控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建完记得复制保存Key 一般只完整显示一次。第三步确认 API 基地址。TaoToken 的 API 入口是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为base_url使用。如果你用的是 OpenAI 官方 SDKbase_url要写到/api这一层SDK 会自动拼接/chat/completions。注意不要把 Key 硬编码进提交到 Git 的脚本里。用环境变量后面代码里我会用os.environ读取。如果你只是想先在网页上验证模型能不能正常对话可以先用模型对话页面 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 发一条消息试试确认 Key 有效再进代码环节。这一步能帮你排除掉“到底是 Key 问题还是代码问题”的干扰。3. 可复制配置tools 定义与多轮循环骨架下面这份代码是整个闭环的核心。我把它拆成三块工具定义、参数校验、Agent 循环。你可以直接建一个agent_demo.py文件跑。3.1 工具定义与模拟服务先定义一个模拟的订单服务避免你真的去连数据库。真实项目里把OrderService换成你的 DAO 或 HTTP 客户端即可。import json import os import time from typing import Any, Dict, List from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlhttps://taotoken.net/api, ) class OrderService: def __init__(self): self.orders { ORD123: {status: 已发货, carrier: 顺丰}, ORD456: {status: 处理中, carrier: None}, } def query(self, order_id: str, phone_last4: str None) - Dict[str, Any]: time.sleep(0.3) if order_id not in self.orders: return {error: 订单不存在} return self.orders[order_id] order_service OrderService()工具 Schema 用 OpenAI 的tools格式定义。注意required字段它是后面“程序强制追问”的依据不是摆设。TOOLS [ { type: function, function: { name: query_order, description: 查询用户的订单状态, parameters: { type: object, properties: { order_id: { type: string, description: 订单号例如 ORD123, }, phone_last4: { type: string, description: 手机号后四位可选, }, }, required: [order_id], }, }, } ]3.2 参数校验防止模型漏传字段模型有时候会因为上下文截断或 prompt 不清晰返回一个缺字段的arguments。如果直接把空值传给下游服务就是静默失败。所以在执行真实函数前加一道断言。def validate_tool_args(tool_name: str, args: Dict[str, Any]) - None: schema next( (t[function] for t in TOOLS if t[function][name] tool_name), None, ) if not schema: raise ValueError(f未知工具: {tool_name}) required schema[parameters].get(required, []) for param in required: if param not in args or not str(args[param]).strip(): raise ValueError(f工具 {tool_name} 缺少必要参数: {param})3.3 多轮循环消息数组怎么驱动这是全文最关键的一段。循环的每一次迭代都是“请求模型 → 判断是否有 tool_calls → 执行 → 回填 → 再请求”。注意roletool消息必须带tool_call_id且要和 assistant 消息里的id对上否则模型无法关联。def run_agent(user_input: str, messages: List[Dict[str, Any]], max_turns: int 5) - str: messages.append({role: user, content: user_input}) turn 0 while turn max_turns: turn 1 response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsTOOLS, tool_choiceauto, ) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content or for tool_call in msg.tool_calls: func_name tool_call.function.name try: args json.loads(tool_call.function.arguments) except json.JSONDecodeError: messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps({error: 参数解析失败}), }) continue try: validate_tool_args(func_name, args) except ValueError as e: messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps({error: str(e)}, ensure_asciiFalse), }) continue if func_name query_order: result order_service.query( order_idargs[order_id], phone_last4args.get(phone_last4), ) else: result {error: 未实现的工具} messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) return 对话轮次超限请简化问题。跑起来的主循环if __name__ __main__: history: List[Dict[str, Any]] [] while True: user_msg input(用户: ).strip() if user_msg.lower() in (quit, exit): break reply run_agent(user_msg, history) print(f助手: {reply}\n)4. 验证请求一次完整的多轮调用长什么样把上面的代码跑起来输入“我的订单到哪了”你会看到消息数组这样生长第一轮模型返回tool_callsarguments是{}因为用户没给订单号。此时validate_tool_args抛错程序把错误信息以roletool回填。模型看到工具报错“缺少 order_id”下一轮就会生成追问“请提供您的订单号”。这就是“程序检测 required 缺失 → 强制追问”的落地方式追问内容由模型基于工具错误生成但触发权在程序手里。第二轮用户输入ORD123。模型这次返回arguments为{order_id: ORD123}校验通过执行order_service.query拿到{status: 已发货, carrier: 顺丰}回填roletool。再次请求模型模型基于这条工具结果生成自然语言回复。第三轮用户追问“那预计什么时候到”。因为history里完整保留了前面所有 user / assistant / tool 消息模型知道订单号是 ORD123、已经查过状态可以直接再发起一次工具调用或基于已有信息回答。验证成功的标志有三个一是追问不是硬编码的而是模型看到工具错误后生成的二是roletool消息的tool_call_id和 assistant 的id完全一致三是多轮之后模型没有“失忆”能引用上一轮的订单号。如果你在网页端想先确认模型对 tools 的支持情况可以用模型对话页面 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 手动构造一条带 tools 的请求观察返回结构里有没有tool_calls字段。这一步能帮你快速判断是模型不支持还是代码写错了。5. 本篇常见错排查5.1 报错tool_call_id不匹配最常见的是回填roletool时忘了带tool_call_id或者自己随便编了一个。正确做法是直接用tool_call.id。如果模型返回多个tool_calls每个都要单独回填一条roletool不能合并成一条。5.2 模型不调用工具直接瞎编答案检查tool_choice是不是设成了none或者 tools 的description写得太模糊。另一个原因是 system prompt 里没有约束“不要猜测用户信息”。可以在 messages 开头加一条 system 消息明确要求参数不全时必须调用工具或追问。5.3 循环停不下来max_turns是必须的兜底。有些模型会在工具报错后反复重试同一个调用没有轮次上限就会死循环。另外如果工具一直返回错误考虑在错误信息里加入“请停止调用并告知用户”的提示。5.4 参数解析失败JSONDecodeError模型的arguments偶尔会返回不合法 JSON尤其是小模型。用try/except包住json.loads失败时回填一条错误 tool 消息让模型重新生成而不是让程序崩溃。5.5 换了模型后 tools 格式不兼容不同厂商对 tools 的字段命名有差异有的叫functions有的叫tools。TaoToken 走的是 OpenAI 兼容格式所以本文代码可以直接用。如果你切到别的通道先确认它接受的是tools还是functions以及返回的是tool_calls还是function_call。6. 继续往下走把闭环接进真实项目到这里一个可多轮调用工具的 Agent 最小闭环已经跑通了。核心就三件事消息数组完整记录三角交互、程序校验 required 参数、roletool正确回填。把OrderService换成你的真实服务把TOOLS换成你的业务函数这套骨架就能直接用。如果你打算长期做编码类或 Agent 类项目频繁调试多轮循环会消耗不少调用量可以了解一下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合这种持续性的开发场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的 base_url 配置示例。Key 的管理统一在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 建议给不同项目建不同的 Key方便排查问题时快速定位。最后留一个我踩过的坑调试多轮循环时把每一轮的messages完整打印出来比只看最终回复有用得多。很多“模型变笨了”的问题其实是你少回填了一条 tool 消息。
