API镜像站搭的 ReAct 智能体跑 Function Calling:Key 用 TaoToken
2026/9/19 0:45:56 网站建设 项目流程

ReAct 智能体跑 Function Calling 时,最容易卡住的地方往往不是模型不会选工具,而是tool_calls回填那一步。Key 用 TaoToken,模型入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 上创建,客户端base_url只认 https://taotoken.net/api,这几个地址填对了,「模型选工具 → 执行函数 → 回填 tool 消息 → 生成最终回答」这四步才不会在第二轮就断掉。原文那套基于 API 镜像站的智能体工程实践,把接口代理层当成整个认知架构的地基,这个判断是对的;但地基下面还有一层更细的东西——请求到底发去了哪个入口、返回体里的tool_calls有没有被中间环节改过结构。这篇文章把原文第 6 章的注册和编排路径,换成一条能直接跟做的配置流程,重点讲清get_weathercalculatesearch_web这三个工具在 ReAct 循环里怎么串起来,以及tool_call_id对不上时该从哪儿查。

1. 从接口代理层到认知架构:ReAct 循环断在 tool_calls 回填

1.1 ReAct 的四步循环,断点通常在第三步

ReAct 这个名字听着抽象,拆开看就是四步:模型根据当前上下文决定要不要调工具、调哪个工具、参数是什么;你的代码真正执行那个函数;把函数返回值以role: "tool"的消息塞回对话历史;模型拿着工具结果生成最终回答。前三步任何一步错位,第四步就永远不会发生。

原文提到「一个tool_calls回填失败就会让整个 Agent 卡住」,这句话的分量在于:它卡的不是一次请求,而是整个循环状态。第一轮模型给了tool_call_id = call_abc123,你回填的时候写成call_abc124,或者干脆自己造了一个 id,第二轮请求发出去,服务端校验消息序列时会直接拒绝——因为role: "tool"的消息必须严格对应前面某条assistant消息里声明的tool_calls。这种错误不会让程序崩溃,只会让 Agent 停在第 2 轮,日志里安安静静。

1.2 接口代理层最容易改坏的三样东西

原文把「接口代理」放在认知架构的最底层,是有道理的:Agent 的认知能力上限,取决于底层通道有没有把原始语义完整传上去、完整带回来。实际踩下来,代理层最容易动坏的是三样东西。

第一是请求路径的拼接方式。很多 OpenAI 兼容 SDK 默认会往base_url后面补/chat/completions,如果你填的base_url本身已经带了/v1,最终路径可能变成/v1/v1/chat/completions,直接 404。

第二是返回体结构的处理。有些中间层会把finish_reasontool_calls改写成stop,或者在有多条工具调用时只保留第一条tool_calls数组元素,模型本来想同时查天气和算数,结果只执行了一个。

第三是流式响应下的分片。开stream=True时,tool_callsarguments是分多个 chunk 拼起来的,拼接顺序错了就会得到一个残缺的 JSON 字符串。

1.3 把模型入口收敛成一条统一 API 通道

解决思路不复杂:让 Agent 只面对一条稳定的 OpenAI 兼容通道,路径、返回体、工具调用结构都由这条通道保证,代码里不再散落多套 Key 和多套域名。TaoToken 在这里承担的就是这个角色——不做别的事,只保证兼容通道的请求和响应结构跟标准一致,tool_calls原样进、原样出。

这样做还有一个附带好处:切模型的时候不用改代码。今天用便宜的小模型跑工具选择,明天换成更强的模型做最终回答,改的只是一个MODEL_ID变量,base_url和 Key 都不动。

2. 第 6.1 节的注册与生成令牌,在 TaoToken 一次做完

2.1 创建 API Key 与确认模型 ID

原文第 6.1 节写的是去镜像站注册、生成令牌、拿到 Key 再回代码里填。这一步现在统一到官网完成:打开 TaoToken 注册账号,进控制台创建一把 API Key,同时在模型广场确认你要用的模型 ID 到底叫什么。

这里有个细节值得单独说:模型 ID 一定要从模型广场当时的列表里复制,不要凭记忆写。原文里那种「随手写个模型名先跑起来」的习惯,在 Function Calling 场景下特别危险——不存在的模型 ID 可能返回的不是 404,而是一个不支持tools参数的普通模型,最后表现为「模型从来不调工具」,排查方向直接被带偏。

拿到两样东西就够了:

  • YOUR_API_KEY:从控制台复制,注意别带上首尾空格;
  • MODEL_ID:从模型广场复制,以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 上的实时列表为准。

2.2 本地依赖与环境变量

原文第 6.2 节的代码基于 OpenAI SDK 风格调用,本地只需要一个包:

pip install "openai>=1.30"

把 Key 和模型 ID 放进环境变量,不要硬编码进仓库:

export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_MODEL_ID="从模型广场复制的模型 ID"

Windows 下用set$env:等价写法即可。如果你的 Agent 要跑在容器或 CI 里,把这两个变量放进密钥管理,而不是写进 Dockerfile。

2.3 base_url 到底填哪个:https://taotoken.net/api 不带 /v1

这是整篇最容易填错的一行。客户端里的base_url只有一个正确写法:

base_url="https://taotoken.net/api"

三个注意点,逐条对应三种常见事故:

  • 末尾不要加/v1。SDK 会自己补/chat/completions,你再加一层路径前缀,得到的就是 404。
  • 不要把官网落地页的地址填进来。https://taotoken.net/?utm_source=taotoken_aicg_blog_end是给人点的,用于注册、创建 Key、看模型广场和用量;填进base_url的永远是https://taotoken.net/api
  • 不要顺手给它加查询参数。utm那串只属于落地页,接口地址保持干净。

3. 第 6.2 节代码改写:ReAct 主循环与三个工具的 schema

3.1 get_weather / calculate / search_web 的工具定义

工具描述写得越实在,模型选错工具的概率越低。原文里这三个工具的名字已经够直白,重点是把参数说明补具体,尤其是单位、格式这类容易含糊的地方。

import json import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url="https://taotoken.net/api", # 末尾不要加 /v1 ) MODEL_ID = os.environ["TAOTOKEN_MODEL_ID"] # 以模型广场列表为准 TOOLS = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气,返回温度和天气状况", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,例如 杭州"}, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位,默认摄氏度", }, }, "required": ["city"], }, }, }, { "type": "function", "function": { "name": "calculate", "description": "计算一个纯数学表达式,例如 (12+8)*3", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "只含数字和四则运算的表达式"} }, "required": ["expression"], }, }, }, { "type": "function", "function": { "name": "search_web", "description": "按关键词检索网页摘要,适合查事实类信息", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "检索关键词"}, "top_k": {"type": "integer", "description": "返回条数,默认 3"}, }, "required": ["query"], }, }, }, ]

工具实现放在本地,模型只负责决定调谁,不负责执行:

def _get_weather(city: str, unit: str = "celsius") -> dict: # 真实项目里在这里接你自己的数据源,返回结构保持简单 return {"city": city, "unit": unit, "temp": 24, "condition": "多云"} def _calculate(expression: str) -> dict: # 只允许数字和四则运算,避免把任意代码交给 eval import re if not re.fullmatch(r"[0-9+\-*/().\s]+", expression): return {"error": "表达式含非法字符"} return {"expression": expression, "result": eval(expression, {"__builtins__": {}})} def _search_web(query: str, top_k: int = 3) -> dict: return {"query": query, "items": [f"{query} 相关摘要 {i}" for i in range(1, top_k + 1)]} TOOL_IMPL = { "get_weather": _get_weather, "calculate": _calculate, "search_web": _search_web, }

3.2 完整可运行的 ReAct 主循环

下面这段是原文第 6.2 节主循环的改写版,关键改动只有两处:base_url指到统一通道,以及assistant消息里的tool_calls原样回填。

def run_react(user_input: str, max_steps: int = 6) -> str: messages = [ { "role": "system", "content": ( "你是一个 ReAct 智能体。需要外部信息时调用工具," "一次可以选择一个或多个工具;调用完成后用简洁中文给出最终回答。" ), }, {"role": "user", "content": user_input}, ] for step in range(max_steps): resp = client.chat.completions.create( model=MODEL_ID, messages=messages, tools=TOOLS, tool_choice="auto", ) msg = resp.choices[0].message # 没有工具调用,说明模型给出了最终答案 if not msg.tool_calls: return msg.content or "" # 1) 把 assistant 这条带 tool_calls 的消息原样写回历史 messages.append({ "role": "assistant", "content": msg.content or "", "tool_calls": [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments, }, } for tc in msg.tool_calls ], }) # 2) 逐个执行工具,并为每一个 tool_call 回填一条 tool 消息 for tc in msg.tool_calls: try: args = json.loads(tc.function.arguments or "{}") except json.JSONDecodeError: args = {} fn = TOOL_IMPL.get(tc.function.name) result = fn(**args) if fn else {"error": f"未知工具 {tc.function.name}"} messages.append({ "role": "tool", "tool_call_id": tc.id, "name": tc.function.name, "content": json.dumps(result, ensure_ascii=False), }) return "已达最大步数仍未收敛,请检查工具返回内容是否让模型无法判断下一步。" if __name__ == "__main__": print(run_react("杭州现在多少度?顺便算一下 (24-18)*3 等于几"))

这几段代码可以让 Claude Code 或 Codex 帮你生成初稿、解释某一行在做什么,但真正执行、观察日志、把报错贴回来,都在你本地终端里做。Agent 跑的只是本地函数,别给它挂能直连生产库的工具。

3.3 tool 角色消息回填,最容易写错的三个细节

第一个细节是tool_call_id必须来自本次响应的tool_calls数组,不能自己生成,也不能跨轮复用。上一轮的 id 用到这一轮,服务端会直接判定消息序列非法。

第二个细节是tool_calls数组有几个元素,就要回填几条tool消息。现在不少模型支持一次返回多个工具调用,比如同时给get_weathercalculate各一条。只回填第一条,剩下的 id 就成了悬空引用。

第三个细节是消息顺序。正确顺序是assistant(含tool_calls)→ 一条或多条tool→ 下一轮请求。中途插一条user消息进去,有些服务端会容忍,有些会直接报序列错误,别赌。

4. 验证 Function Calling 链路:从单工具到三轮串联

4.1 单工具冒烟:只留 get_weather

新链路第一次跑,别一上来就三个工具全开。把TOOLS临时裁到只剩get_weather,输入「杭州现在天气怎么样」,观察两件事:第一轮响应的finish_reason是不是tool_calls,回填之后第二轮是不是返回了自然语言答案。

这一步能过,说明 Key、base_url、模型 ID、工具 schema 四样东西都是通的。很多人跳过这步直接跑复杂查询,结果分不清是工具定义写错了还是入口填错了。

4.2 多工具串联:查天气 → 计算 → 搜网页

单工具通了之后,把三个工具放回去,换成需要多轮编排的问题,比如「查一下杭州天气,再算一下今天和昨天的温差,最后搜一下这个温差算不算异常」。理想情况下你能在日志里看到 2 到 3 轮工具调用,每轮都有对应的tool消息回填,最后收敛成一段自然语言。

如果模型倾向于一次把三个工具全调了,而不是一轮一个,这也是正常的——它只是在做并行工具调用。你的循环只要保证每个tool_call_id都有一条tool消息接住,就不会出问题。

4.3 日志里必须盯住的四个字段

字段位置该看什么
finish_reasonchoices[0]tool_calls还是stop,决定循环走向
tool_calls[].idassistant 消息和回填的tool_call_id是否一一对应
tool_calls[].function.nameassistant 消息是否落在你注册的三个工具里
tool_calls[].function.argumentsassistant 消息是不是合法 JSON,有没有被截断

把这四个字段打成一行日志,排障时能省掉一大半时间。

提示:开发阶段把每一轮的完整messages数组也打出来。看着历史里assistant(tool_calls)tool是否成对出现,比读任何文档都直观。

5. 报错逐条对照:tool_call_id、401、404 与 arguments 截断

5.1 tool_call_id 匹配失败

典型报错长这样:Invalid parameter: messages with role 'tool' must be a response to a preceding message with 'tool_calls',或者更直白的tool_call_id not found

原因基本跑不出三种:assistant那条消息没有把tool_calls带回去(只写了content)、tool_call_id被自己改写或复用了旧值、多个工具调用只回填了一条。照着 3.3 节那三个细节逐条对一遍,基本都能解决。

还有一种隐蔽情况:有的实现喜欢在回填后再append一条自己拼的assistant消息,导致tool消息后面跟的不是模型输出而是人工构造的消息,序列校验同样会挂。

5.2 401 与 404:Key 和 base_url 各查一遍

401 基本只跟 Key 有关:YOUR_API_KEY是不是从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 控制台复制时带了空格、是不是用了已删除的旧 Key、环境变量名有没有拼错导致读到空字符串。顺手print(len(api_key))看一眼长度,比猜快得多。

404 几乎只跟路径有关:base_url写成了https://taotoken.net/api/v1,或者误把官网落地页那串带utm的完整地址填了进去。改回https://taotoken.net/api,去掉多余后缀就好。

5.3 arguments 不是合法 JSON 怎么办

json.loads(tc.function.arguments)JSONDecodeError,通常有三种来源:模型在参数里塞了注释或多余文本、开了流式响应但分片拼接顺序不对、参数太长被截断。

处理顺序建议是先降复杂度——把工具参数从嵌套对象改成扁平字符串,观察是否还出错;如果只在流式下出问题,就检查 chunk 累积逻辑,arguments必须按到达顺序拼接,不能按 index 乱序处理。代码里给json.loads包一层 try,失败时把原始字符串原样回给模型,让它重新组织参数,比直接抛异常把 Agent 打断要温和得多。

5.4 模型不返回 tool_calls 的两种情形

一种是模型本身不支持tools参数,只是默默忽略,表现为永远直接回答、永远不进工具分支。这时回模型广场确认一下你选的模型 ID 是否支持函数调用。

另一种是工具描述写得太模糊,模型觉得没必要调。把description写具体,比如get_weather明确写「返回温度和天气状况」,比「获取天气信息」更容易被选中。

6. 循环跑顺之后,去把这次调用对上账

6.1 用同一把 Key 在模型对话里验一次

Agent 跑通不代表 Key 和模型 ID 就一定填对了——有可能你本地用的是缓存的历史响应。稳妥的做法是拿同一把YOUR_API_KEY,去 TaoToken 模型对话 里发一条普通消息,确认返回正常。如果对话能用、Agent 报 401,问题一定出在你的环境变量或客户端初始化上,跟通道无关。

6.2 长期跑 Agent 的话,看 Coding Plan 和 Key 管理

ReAct 循环的特点是请求数翻倍——一次用户提问可能触发三四轮模型调用,每轮都带完整历史。跑几天之后,最该做的不是优化 prompt,而是回控制台看用量曲线,确认每轮请求都被正确记账。

如果这个 Agent 要长期挂着跑,可以看 Coding Plan 的额度是否够用;需要给多个 Agent 分配不同 Key 做隔离时,在 控制台 API Keys 里新建,别让调试和生产共用一把。要是你打算把同样的编排逻辑搬到命令行工具里跑,环境变量的对应关系可以对照 Claude Code 接入文档 里的字段说明改,base_url那一栏同样是https://taotoken.net/api,不带/v1

整条链路里真正需要你反复检查的,其实就三样:base_url有没有多后缀、tool_call_id有没有配对、tool_calls数组有没有漏回填。把这三样盯住,ReAct 循环基本不会再莫名其妙停在第 2 轮。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询