Agent 跑 Function Calling,Base URL 和 Key 千万别散落两处。先到 TaoToken 这边:https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册、创建 Key,然后回到 04、05、06 三节里的示例代码,把模型请求的入口统一成https://taotoken.net/api。这么做不是为了少写两行配置,而是为了让「第一次带 tools 的请求」和「工具跑完后的第二次请求」走同一条路。
很多人照着原文敲完messages、role、tools,单步调试都能过,一旦把 Agent 串起来就出问题:文字接龙那段写的 Demo 用了一个地址,Function Calling 那段抄来的代码又用了另一个地址,最后 Agent 里加了个query_order工具,返回值拼回messages时忘了换成同一个通道。报错看起来像 messages 拼错,其实是请求打到了不同地方。这篇把接入这一件事说透,概念部分还是回到原文那三节去啃。
1. 04 里的 messages 与 role,真正卡住人的是「第二次请求」
1.1 文字接龙示例:messages 是唯一的上下文载体
原文 04 用文字接龙讲messages,这个类比很准:模型自己不带记忆,你每次把整个数组重新发一遍,它才「记得」前面说了什么。role只有三种基础角色,system定规矩,user提要求,assistant是模型自己的历史发言。数组的顺序就是对话的时间线,顺序错了,模型接的话就会跑偏。
这套机制在单轮对话里没毛病,问题出在 Function Calling 上。一次带工具的任务至少要发两次请求:第一次把tools一起发过去,让模型决定调哪个函数;第二次把工具的执行结果追加进messages,再问一遍。两次请求面对的是同一份上下文数组,如果第一次走 A 通道、第二次走 B 通道,模型看到的内容没变,但后面统计用量、看日志、排查超时的时候,你会发现两次请求根本对不上号。
1.2 tools 声明与 role: "tool" 是同一套协议的两半
tools里描述的是「有什么函数可用」,参数结构用 JSON Schema 写;模型不会真的去执行你的函数,它只是在回复里吐出tool_calls,告诉你「我想调query_order,参数是{"order_id": "A10086"}」。真正执行的是你自己的代码,执行完把结果包成{"role": "tool", "tool_call_id": ..., "content": ...}塞回messages。
这就是为什么接入配置必须先定下来。role: "tool"这条消息只对「同一次会话的后续请求」有意义,tool_call_id也是上一轮返回的 ID。换通道等于换了一个不认这个 ID 的服务端,模型会当成一堆无意义的文本处理,回复自然驴唇不对马嘴。
1.3 把 Key 和 Base URL 从业务代码里抽出来
写 Demo 时最省事的做法是把 Key 硬编码在client = OpenAI(api_key="sk-xxx")里,再顺手把base_url也写死。等要用 Agent 跑真实流程,代码里可能有三四个文件各有一份配置,改一次地址要全局搜索。正确姿势是抽两个环境变量:TAOTOKEN_API_KEY和TAOTOKEN_MODEL,代码里只留base_url="https://taotoken.net/api"。这样第一次请求和第二次请求共用同一个 client 实例,通道天然统一。
2. 去 TaoToken 创建 Key,把 base_url 定成 https://taotoken.net/api
2.1 准备材料:一把 Key 和一个模型 ID
打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 完成注册,进控制台创建 API Key,复制出来后写进环境变量,代码里一律用占位符YOUR_API_KEY,别把真 Key 提交到仓库。
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_MODEL="YOUR_MODEL_ID"模型 ID 不用猜,也不要用别人博客里的旧名字。去 TaoToken 的模型广场看你当前账号可用的列表,复制对应 ID 填进TAOTOKEN_MODEL。模型上下架是会变的,以模型广场当时列表为准,比记名字靠谱。
2.2 base_url 填 https://taotoken.net/api,不要写官网地址,也不要加 /v1
这是最容易搞混的一点。给人点的链接是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,填进代码里的base_url是https://taotoken.net/api,两者不是一个东西。把官网地址填进base_url,请求会打到页面上;在https://taotoken.net/api后面再补一个/v1,路径会变成双份版本号,通常直接 404。
提示:判断标准很简单——凡是「注册、创建 Key、看模型列表、看用量」,都是官网地址;凡是「填进 OpenAI SDK、Codex、Claude Code 的参数」,都是
https://taotoken.net/api,末尾不带斜杠也不带/v1。
2.3 通道统一之后,原文的 tools 逻辑一行都不用改
TaoToken 在这里只负责一件事:给 Agent 的每一次模型请求提供同一把 Key 和同一个入口。原文里的messages怎么拼、role怎么排、tools的 schema 怎么写,全部保持原样。你要改的只有base_url和api_key两个参数,加上把模型 ID 换成模型广场里的真实值。
3. 把 05 的 Function Calling 示例接到这条通道上
3.1 第一次请求:声明 tools,模型只回 tool_calls
先照着原文的结构,写一个query_order的工具声明,然后用同一个 client 发第一次请求:
import json import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url="https://taotoken.net/api", ) tools = [ { "type": "function", "function": { "name": "query_order", "description": "按订单号查询订单当前状态,返回 JSON 字符串", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "订单号"} }, "required": ["order_id"], }, }, } ] messages = [ {"role": "system", "content": "你是订单助手,需要查订单时调用 query_order。"}, {"role": "user", "content": "帮我看看订单 A10086 现在是什么状态"}, ] resp = client.chat.completions.create( model=os.environ["TAOTOKEN_MODEL"], messages=messages, tools=tools, tool_choice="auto", ) msg = resp.choices[0].message print(msg.tool_calls)第一次请求结束后,msg.content通常是空的,msg.tool_calls里有模型挑中的函数名和参数。这一步没有任何工具被执行,模型只是「点了菜」。
3.2 第二次请求:把工具结果追加进 messages 再问一遍
原文 05 的重点就在这一步。工具是你自己执行的,模型碰不到你的订单库。给它一个本地桩函数,把返回值包成role: "tool"追加进去:
def query_order(order_id: str) -> str: # 演示用桩函数。真实项目里这一步在你的服务端/RPC 里跑, # 不要让模型直接连生产库,也不要让它生成 SQL 去执行。 return json.dumps( {"order_id": order_id, "status": "已发货", "carrier": "顺丰"}, ensure_ascii=False, ) messages.append(msg.model_dump(exclude_none=True)) for call in msg.tool_calls: args = json.loads(call.function.arguments) result = query_order(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) final = client.chat.completions.create( model=os.environ["TAOTOKEN_MODEL"], messages=messages, tools=tools, ) print(final.choices[0].message.content)第二次请求的messages里多了两条:一条是带tool_calls的 assistant 消息,一条是role: "tool"的结果消息。这两条必须成对出现,tool_call_id也要对得上,模型才知道「我刚才点的那个单,结果回来了」。
3.3 query_order 绕一圈之后,通道始终没变
从用户提问,到模型决定调用query_order,到你的代码执行查询,再到把结果拼回上下文发起第二次请求,整条链路里模型请求只发生两次,且都由同一个 client 发出。原文 06 把这条链路升级成 Agent 之后,步骤会变多,但「每次模型请求都走同一个入口」这个前提不能破。
注意:诊断类 SQL、编译、跑脚本这些动作,都放在你本地或者测试环境里执行,把输出或报错贴回对话,让模型帮你解释。不要写成「让 Agent 连上库自己执行」——这既不是原文的意图,也不安全。
4. 06 的 Agent:模型、记忆、规划、工具共用一条通道
4.1 上下文记忆就是 messages 数组别丢
原文 06 把「上下文记忆」列成 Agent 的一个组成部件,落到代码层面它就是那个不断变长的messages数组。多轮工具调用之后,数组里会堆着 user、assistant、tool 三种角色的消息,顺序和配对关系一个都不能乱。如果中间某次请求换了通道,服务端返回的tool_calls结构可能对不上,tool_call_id就断了链。
所以把 client 做成模块级单例,把base_url和 Key 从环境变量读,是所有 Agent 示例都该有的基础习惯。这件事做对以后,记忆那部分你不用额外写持久化逻辑也能跑通单次会话。
4.2 任务规划,本质是多次 Function Calling 的排列
「任务规划」听起来玄,实际就是让模型把一个大目标拆成几步,每一步对应一次或多次工具调用。比如「查一下 A10086 状态,如果已发货就把物流单号也取出来」,模型可能规划成先调query_order,拿到carrier之后再调query_logistics。每一轮都是「发请求 → 拿 tool_calls → 本地执行 → 拼回 messages → 再发请求」这个循环。
这正好说明为什么接入必须统一:循环每多跑一圈,就多一次模型请求。如果配置散落在不同文件,跑到第三轮时你自己都不确定这次用的是哪把 Key、哪个模型。把入口固定成https://taotoken.net/api,循环跑多少轮都只需要看一处配置。
4.3 工具数量增多时,schema 别顺手改坏
原文提到「工具调用」时用的例子很精简。真实 Agent 里 tools 会有五六个,这时候description写得含糊,模型就容易挑错函数或者传错参数。参数 JSON Schema 里required要写清楚,别指望模型自己补默认值。这一步和接入通道无关,但和接入错误长得很像——模型开始胡说八道时,先检查tools声明,再检查base_url和模型 ID。
5. 跑完整 Agent 之前,先用最小 chat 请求验一次
5.1 curl 一条最小请求,确认地址和 Key 都没填错
正式写业务逻辑前,先用命令行打一发最简单的请求。这一步能把「地址写错」「Key 无效」「模型名不存在」三个问题一次性暴露出来:
curl https://taotoken.net/api/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "YOUR_MODEL_ID", "messages": [{"role": "user", "content": "只回复两个字:收到"}] }'能正常拿到回复,说明 Key 有效、https://taotoken.net/api这个入口可用、模型 ID 也选对了。接下来再跑第 3 节那段带 tools 的代码,出问题的范围就缩小到messages拼装和tool_call_id配对上了。
5.2 Python 里加两个断言,避免把错误配置带进 Agent
环境变量最容易出现的问题是多打一个空格、复制时带上引号。在脚本开头加两行检查,比事后猜半天强:
import os assert os.environ.get("TAOTOKEN_API_KEY"), "缺少 TAOTOKEN_API_KEY,去官网控制台创建一个" assert not os.environ["TAOTOKEN_API_KEY"].startswith("http"), "这里应该填 Key,不是地址"第二条断言是有用的:把地址和 Key 填反是常见错误,尤其在 Agent 代码里同时有两个环境变量的时候。
5.3 第二次请求失败,先看 tool_call_id
如果第一次请求成功、第二次请求报参数错误,八成是tool_call_id没对上。检查两点:messages.append(msg.model_dump(exclude_none=True))有没有漏掉;循环里是不是给每条tool_calls都补了一条role: "tool"的消息。模型返回几个tool_calls,你就得补几条结果消息,数量必须一致。
6. 排障:Agent 链路上常见的四类报错
6.1 401 和 404 分工明确
401 基本都是 Key 的问题:Key 没带、带错、复制时多了引号或空格,或者Authorization头写成了别的字段。404 基本都是地址的问题:base_url填成了官网地址,或者末尾多加了/v1。这两类错误不会出现在第二次请求里只出现在第一次,如果第一次能通、第二次报错,那基本可以排掉这两项。
6.2 模型 ID 报错与模型广场对不上
报「模型不存在」时,先确认你填的 ID 是模型广场里当前可用的,而不是从别的文章抄来的旧名字或自己拼的后缀。模型上下架会调整,改配置前习惯性打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 看一眼列表,比反复试错快得多。
6.3 返回内容里混了工具调用的文本
有时候模型没有产出标准tool_calls,而是在content里写了一段类似 JSON 的文字。这通常意味着tools的 schema 描述不够清楚,或者tool_choice设置得太松。先把函数描述写具体,再考虑把tool_choice限定成具体函数名做对照测试。
6.4 第一次通、第二次超时或空回复
排查顺序是:messages是不是太长,工具返回的content是不是塞了一大段原始日志,tools是不是每次请求都完整带上。第二次请求同样需要tools参数,漏掉的话模型会以为工具已经不可用了,只能硬答。
7. 跑通之后,把调试壳和用量对一遍
7.1 用 Claude Code 或 Codex 当调试壳时,填的还是同一个入口
如果你习惯在命令行里对着代码改 Agent 逻辑,可以把 Claude Code 的~/.claude/settings.json指到同一条通道,注意这里填的是接口地址,不是官网地址:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_MODEL_ID" } }Codex 那边对应的是~/.codex/config.toml,注意它用的是另一套变量名,别把ANTHROPIC_*套过来:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"这两个壳都只帮你写代码、读报错、改配置,不会替你连生产库或执行诊断脚本。跑 SQL、编译、重启服务这些动作,仍然是在你自己机器上执行,把结果贴回来继续问。
7.2 下一步:先看这次调用有没有记上账
配置保存后,去 TaoToken 模型对话 用同一把 Key 发一条测试消息,确认模型 ID 和地址都对得上。如果想长期拿它跑 Agent 调试,可以在 Coding Plan 里看套餐是否够用;Key 不够就再建几把,入口在 控制台 API Keys。Claude Code 的环境变量写法对照 接入文档 更省事。
回到 Agent 本身,下一步其实不是继续堆工具,而是把messages的拼装逻辑单独抽成一个函数,把工具执行的部分抽成注册表。这样以后加第十个工具、换模型、改地址,都只动一处。工具越多,配置越要收口,这话在第一次跑query_order的时候就该记住。