1. 为什么我劝你先搭一个最小 Agent Harness,而不是急着换模型
很多人第一次接触 Agent,注意力全在模型上:哪个模型工具调用强、哪个模型指令跟随好、哪个模型便宜。但真把 Agent 跑起来之后你会发现,模型只是其中一环。同一个模型,换一套工具描述、换一种结果回填格式,成功率能差出一大截。问题往往不在模型,而在模型外面那层「壳」。
这层壳就是 Agent Harness。你可以把它理解成给模型搭的一个小型工作台:任务从哪进来、有哪些工具可以用、工具怎么被调用、调用结果怎么塞回对话、最后怎么判断这次任务算不算完成。模型负责「想」,Harness 负责「让它能想、能动手、能留下痕迹」。
我见过太多人卡在这一步:写了个 while 循环,把工具列表塞进 system prompt,模型返回一段 JSON,解析出来执行,再把结果拼回 messages,然后……就没有然后了。跑两次发现模型开始胡编工具名,或者参数格式对不上,或者工具报错了模型不知道,最后只能放弃,回去手动复制粘贴。
这篇要解决的就是这个。我会带你从零搭一个最小可运行的 Agent Harness,包含工具注册、调用分发、结果回填三个核心环节,模型侧请求统一走 TaoToken 的 Key 和 API 通道。目标很明确:本地跑通一条完整的工具调用链路,并且这个骨架是可以往上加东西的。
适合谁看:写过一点 Python、调过 OpenAI 兼容接口、想让 Agent 真正跑起来而不是停在 demo 阶段的人。不需要你有框架经验,LangChain、AutoGPT 那些可以先放一边,我们从最裸的 HTTP 请求开始。
先说清楚「最小」的边界。一个能用的 Harness 至少要有四样东西:一份工具注册表(告诉模型有哪些工具、参数长什么样)、一个分发器(根据模型返回的调用意图去执行对应函数)、一个结果回填机制(把执行结果按模型能理解的格式塞回去)、一个循环控制(什么时候继续、什么时候停)。评分器、Trace 记录这些可以后面加,但上面四个缺一个都跑不通。
我试过用最朴素的方式实现这四样,代码量其实很小,核心逻辑不到两百行。难点不在写代码,而在几个容易踩的细节:工具 schema 怎么写模型才认、并行调用怎么处理、工具报错要不要抛给模型、循环什么时候该强制中断。下面一步步来。
2. TaoToken 前置准备:统一 Key 与 API 通道,把模型侧请求先打通
在写 Harness 之前,得先保证模型侧能稳定调通。Harness 本身不关心你用哪家模型,它只关心「我发一个带 tools 的请求,你能不能返回规范的 tool_calls」。所以我们需要一个 OpenAI 兼容的接口通道,把 Key、Base URL、Model ID 三件事定下来。
TaoToken 在这里的角色就是统一入口:一个 Key,一个 Base URL,切换模型只改 Model ID。对 Harness 来说这很省事,因为工具调用的请求格式是统一的,不用为每家模型写一套适配。
先拿 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台里创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 列表在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。创建完记得复制保存,页面刷新后一般不再完整显示。
Base URL 用 https://taotoken.net/api ,注意这个地址后面不加任何 UTM 参数,直接作为 OpenAI SDK 的 base_url 使用。Model ID 根据你选的模型填,比如工具调用能力比较稳的通用模型。具体有哪些模型可以在模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 里先试一下,确认这个模型支持 function calling 再写进代码。
环境变量这样设,避免 Key 硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="你的模型ID"如果你用 Claude Code 这类工具做辅助开发,接入配置也是同一套三件套。Base URL 填 https://taotoken.net/api ,Key 填上面创建的,Model ID 填你选的模型。Claude Code 的接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有不同客户端的配置示例,照着改就行。
这里有个前置检查很重要:先用一个最简单的请求确认通道是通的,别等 Harness 写完才发现 Key 有问题。用 curl 测一下:
curl https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "'"$TAOTOKEN_MODEL"'", "messages": [{"role": "user", "content": "只回复两个字:通了"}] }'返回里有 choices 且 content 是「通了」,说明 Key、Base URL、Model ID 三件套没问题。如果这里就报 401,先回去检查 Key 有没有复制全、有没有多余空格。这一步过了,再往下写 Harness,排障范围会小很多。
顺便说下为什么建议用统一通道而不是每个模型单独配。Harness 调试阶段你会频繁换模型对比工具调用效果,如果每换一个模型就要改 base_url、改鉴权方式、改请求体格式,调试成本会很高。统一通道下,换模型只改一个字符串,Harness 代码一行不动。
3. 可复制配置:目录结构、工具注册表与 settings 片段
这一节给可以直接抄的结构和配置。先看目录,保持扁平,别一上来就分层:
mini-harness/ ├── main.py # 入口,跑一次完整链路 ├── harness.py # 核心循环:请求、分发、回填 ├── tools.py # 工具注册表 + 具体实现 ├── config.py # 读取环境变量 └── requirements.txtrequirements.txt 只需要一个依赖:
openai>=1.30.0config.py 负责把环境变量收拢:
import os API_KEY = os.environ["TAOTOKEN_API_KEY"] BASE_URL = os.environ.get("TAOTOKEN_BASE_URL", "https://taotoken.net/api") MODEL = os.environ["TAOTOKEN_MODEL"]重点是 tools.py。工具注册表要同时承担两件事:给模型看的 schema,和本地实际执行的函数。我用一个字典把两者绑在一起,避免 schema 和实现脱节:
import json def list_files(path: str = ".") -> str: import os try: return json.dumps(os.listdir(path), ensure_ascii=False) except Exception as e: return json.dumps({"error": str(e)}, ensure_ascii=False) def read_file(path: str) -> str: try: with open(path, "r", encoding="utf-8") as f: return f.read()[:4000] except Exception as e: return json.dumps({"error": str(e)}, ensure_ascii=False) TOOL_REGISTRY = { "list_files": { "fn": list_files, "schema": { "type": "function", "function": { "name": "list_files", "description": "列出指定目录下的文件名", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "目录路径,默认当前目录"} }, "required": [] } } } }, "read_file": { "fn": read_file, "schema": { "type": "function", "function": { "name": "read_file", "description": "读取指定文本文件的内容", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "文件路径"} }, "required": ["path"] } } } } } def get_tool_schemas(): return [v["schema"] for v in TOOL_REGISTRY.values()] def dispatch(name: str, arguments: str) -> str: if name not in TOOL_REGISTRY: return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False) try: args = json.loads(arguments) if arguments else {} except json.JSONDecodeError: return json.dumps({"error": "参数不是合法 JSON"}, ensure_ascii=False) return TOOL_REGISTRY[name]["fn"](**args)这段代码有两个设计点值得说。第一,工具执行永远返回字符串,报错也包成 JSON 字符串返回,而不是抛异常。原因是工具报错本身也是给模型的信息,模型看到 error 字段后有机会自己纠正,比如换个路径重试。如果你直接抛异常中断循环,模型就失去了纠错机会。第二,dispatch 里对未知工具和非法 JSON 都做了兜底,因为模型偶尔会编出不存在的工具名或者返回坏 JSON,Harness 不能因此崩掉。
如果你用 Cline 或者带 MCP 的客户端,工具注册的思路是一样的,只是 schema 换成 MCP 的格式。核心三件套不变:Base URL 用 https://taotoken.net/api ,Key 用你的 TaoToken Key,Model ID 填支持工具调用的模型。MCP 配置里把这三项填对,工具列表就能正常暴露给模型。
再给一个 settings 片段,方便你在支持 JSON 配置的客户端里直接粘贴:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的key", "model": "你的模型ID", "tools_enabled": true }注意 base_url 不要写成带路径的形式,OpenAI SDK 会自动拼 /chat/completions。如果你手动拼 URL,就是 https://taotoken.net/api/chat/completions 。
4. 验证请求:跑通一次完整的工具调用链路
配置齐了,现在写核心循环 harness.py。逻辑是:把用户任务和工具 schema 发给模型,如果模型返回 tool_calls 就执行、回填、再请求,直到模型给出最终回答或者达到最大轮数。
from openai import OpenAI import json import config from tools import get_tool_schemas, dispatch client = OpenAI(api_key=config.API_KEY, base_url=config.BASE_URL) def run(task: str, max_turns: int = 6): messages = [{"role": "user", "content": task}] trace = [] for turn in range(max_turns): resp = client.chat.completions.create( model=config.MODEL, messages=messages, tools=get_tool_schemas(), tool_choice="auto" ) msg = resp.choices[0].message messages.append(msg) if not msg.tool_calls: return {"answer": msg.content, "trace": trace} for call in msg.tool_calls: name = call.function.name args = call.function.arguments result = dispatch(name, args) trace.append({"tool": name, "arguments": args, "result": result}) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result }) return {"answer": "达到最大轮数,强制停止", "trace": trace}几个关键点。第一,messages.append(msg)这步不能省,模型的 tool_calls 消息必须进历史,否则下一轮请求里 tool 消息找不到对应的 call_id,接口会报错。第二,每条 tool 消息必须带tool_call_id,且要和对应 call 的 id 完全一致,这是最容易写错的地方。第三,max_turns 是保险丝,防止模型陷入无限调用。
现在造一个测试环境,验证链路。建两个文件:
mkdir -p demo_env && cd demo_env echo "本项目支持本地启动、基础登录和配置管理。" > README.md echo "配置项包括 port、theme、log_level。" > config.md回到项目根目录,写 main.py:
from harness import run task = "请先列出当前目录的文件,然后读取 README.md,判断这个项目是否支持插件系统。" result = run(task) print("最终回答:", result["answer"]) print("调用轨迹:") for step in result["trace"]: print(" -", step["tool"], step["arguments"], "->", step["result"][:80])跑起来:
python main.py预期你会看到类似这样的输出:模型先调用 list_files 拿到文件列表,再调用 read_file 读 README.md,最后给出「README 中没有提到插件系统,不能确认支持」这类回答。trace 里会记录两次工具调用,参数和结果都在。
这条链路跑通意味着什么?意味着模型侧请求、工具 schema 传递、tool_calls 解析、本地执行、结果回填、二次请求这六个环节全部打通。后面你要加工具,只需要往 TOOL_REGISTRY 里加一项;要加评分,就在 run 返回后接一个 grader;要记录更细的 trace,就在 dispatch 前后加日志。
如果你想先在网页上确认模型对工具调用的支持情况,可以去模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 手动发一条带工具描述的消息,看看返回结构。网页端能直观看到 tool_calls 长什么样,对理解代码里的解析逻辑有帮助。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
链路跑不通时,报错信息往往指向不同层。这一节按真实遇到的错误对照排查,每个都给出定位方法。
401 Unauthorized。这是鉴权层的问题,和 Harness 代码无关。先确认环境变量有没有生效:echo $TAOTOKEN_API_KEY,看输出是不是完整 Key。常见坑是复制时带了换行或空格,或者用了别的项目的 Key。如果 Key 确认没问题,检查 base_url 是不是写成了 https://taotoken.net/api 而不是别的路径。401 基本就这两处,Key 和 Base URL。
local proxy failed / connection error。这类报错说明请求根本没发出去,卡在本地网络层。先确认你的机器能正常访问外网,用 curl 测一下 base_url 的连通性。如果你本地配了什么网络工具,检查它有没有拦截这个域名的请求。注意,Harness 本身不需要任何特殊网络配置,它就是一个普通的 HTTPS 请求。如果 curl 能通但 Python 不通,多半是 Python 环境里设了额外的代理变量,检查HTTP_PROXY、HTTPS_PROXY这两个环境变量,临时清掉再试。
reading 'choices' / KeyError: 'choices'。这个报错说明请求发出去了,但返回体里没有 choices 字段,代码在resp.choices[0]处崩了。原因通常是接口返回了一个错误对象,比如{"error": {"message": "..."}}。定位方法是在 create 之后先把原始返回打出来:
resp = client.chat.completions.create(...) print(resp.model_dump())看 error 里的 message 是什么。常见的有模型 ID 写错、模型不支持 tools 参数、请求体格式不对。如果是模型不支持工具调用,换一个支持 function calling 的模型 ID 再试。
OAuth / authentication 相关报错。如果你在 Claude Code 或类似客户端里看到 OAuth 报错,说明客户端在走它自己的登录流程,而不是用你配的 Key。这时候要检查客户端的配置是不是真的读到了你填的 Base URL 和 Key。Claude Code 的接入方式在文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里有说明,确认配置项名称和位置对不对。有些客户端需要显式指定使用 API Key 模式而不是 OAuth 模式。
工具调用相关报错。如果报tool_call_id not found或者messages with role 'tool' must be a response to a preceding message with 'tool_calls',说明回填顺序错了。检查两点:模型的 tool_calls 消息有没有 append 进 messages;每条 tool 消息的 tool_call_id 是不是和 call.id 一致。这两个对上了,报错就消失。
模型不调用工具,直接回答。这不是报错,但很常见。原因可能是工具 description 写得太模糊,模型不知道什么时候该用。把 description 写具体,比如「列出指定目录下的文件名」比「文件操作」好得多。另外 tool_choice 设成 "auto" 时模型有选择权,如果任务本身不需要工具,它直接回答是正常的。
排查顺序建议固定下来:先 curl 测通道,再打印原始返回看 error,再检查 messages 结构,最后才怀疑模型。按这个顺序走,大部分问题五分钟内能定位。
6. 把骨架用起来:从最小 Harness 到可扩展的评测环境
链路跑通之后,这个骨架的价值才开始显现。它不只是一个能调工具的 demo,而是一个可以往上叠能力的底座。
第一层扩展是加工具。往 TOOL_REGISTRY 里加一项,schema 和实现一起写,模型下一轮请求就能看到新工具。比如加一个 run_tests 工具去执行测试脚本,加一个 write_file 工具去落盘,加一个 search 工具去检索。工具越多,越要注意 description 的区分度,否则模型会在相似工具之间选错。
第二层扩展是加 Trace 和评分。现在 trace 只记了工具名、参数、结果,你可以加上时间戳、轮次、token 消耗。评分器可以先用规则:任务要求读 README,就检查 trace 里有没有 read_file 且路径是 README.md;要求不能超出文件内容下结论,就检查最终回答里有没有出现文件里没有的关键词。规则评分虽然粗糙,但足够定位大部分问题。
第三层扩展是批量跑 case。把任务、环境、评分规则写成 JSON,一个 case 一个文件,Harness 循环读取、执行、评分、汇总。这时候你就有了一套自己的 Agent 评测流程,能对比不同模型、不同工具描述、不同 prompt 下的表现差异。
回到最开始那个判断:Agent 的问题很少单纯出在模型上。工具描述不清、结果回填格式不对、循环控制缺失、报错没反馈给模型,任何一个环节出问题,表现都会像「模型不行」。有了 Harness,你能把问题拆开看:是模型没选对工具,还是选了但参数填错,还是执行成功但模型没用结果,还是评分规则本身不合理。这些区分,靠手动试用是看不出来的。
如果你打算长期做 Agent 相关的开发,建议把 Coding Plan 也了解一下,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,适合需要持续跑编码类 Agent 任务的场景。日常调试和验证模型工具调用能力,用模型对话页就够了。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到配置问题先翻文档,大部分客户端接入方式都有示例。
最后留一个实用建议:Harness 的第一版一定要小,小到你能一眼看完整个循环。别一上来就上框架、上抽象层、上插件系统。先把「请求—分发—回填—再请求」这条线跑顺,把 trace 打出来看几遍,你会对 Agent 到底怎么工作有完全不一样的理解。等这条线稳了,再往上加东西,每一步都知道自己在加什么。