1. 从“只会说”到“真的做”:ReAct 循环里缺的那双手
如果你跟着前几篇把 ReAct 循环搭起来了,大概率会遇到一个很别扭的场景:模型在 Thought 里写得头头是道,Action 也规规矩矩输出read_file('main.py'),可你心里清楚——这行字只是被打印出来了,文件根本没被打开。Agent 像一个口才很好的实习生,能复述流程,却动不了手。
这一篇要解决的就是这个断层:让 Claude Code 风格的 Agent 真正具备读写文件能力,也就是把Execute(Action)从硬编码的假数据,换成真实的文件系统操作。同时,我会用 TaoToken 作为统一的 API 通道,把模型调用和工具调用串成一条链路,避免你在多个 Key、多个 Base URL 之间来回切换。
核心检索词先摆出来:Claude Code 工具调用、ReAct 读写文件、Agent 文件操作配置。这三件事其实是同一个闭环的三面——模型负责想,工具负责做,通道负责把两边接起来。适合谁看?适合已经写过一点 Python、知道什么是 API Key、但还没亲手让 Agent 碰过真实文件的零基础读者。你不需要懂 Rust,也不需要读过 Claude Code 源码,只要能把一段 JSON 配置抄对、把一次读写跑通,这篇的目标就达成了。
我试过把工具调用拆成“模型输出字符串 → 解析 → 执行 → 回填 Observation”四步之后,排错会变得非常直观:哪一步断了,日志里一眼能看出来。下面按这个顺序推进,先讲清楚闭环,再给可复制的配置骨架,最后用一次真实的读写动作验证链路。
2. TaoToken 前置:统一 Key 与 API 通道,别让工具调用卡在鉴权上
在给 Agent 装“双手”之前,得先保证它的“嘴”是通的。工具调用和普通对话最大的区别是:一次任务里模型可能被调用多轮,每轮都要带上工具定义、历史消息、Observation。如果每换一个模型就换一套 Key 和 Base URL,配置会迅速失控。TaoToken 在这里的作用就是提供统一的 API 通道,让你用同一个 Key 去访问不同模型,配置只写一份。
先把入口记清楚,后面配置里会反复用到:
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 地址:https://taotoken.net/api (这个不加 UTM,直接作为 Base URL 用)
- 模型对话页:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
- Claude Code 接入说明:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite
为什么强调“统一 Key”?因为工具调用链路里有两个地方要鉴权:一是模型请求,二是如果你用了某些托管式工具网关,工具侧也可能要鉴权。把模型侧收敛到 TaoToken 一个通道,至少能砍掉一半变量。你只需要在 API Keys 页面生成一个 Key,然后在配置里写死 Base URL 和 Model ID,剩下的交给代码。
这里要提醒一句:TaoToken 是 API 通道,不是编辑器替代品。Claude Code 的“双手”仍然是你本地的 Python 函数,TaoToken 负责的是让模型能稳定地被调用、能返回结构化的工具调用意图。两者职责别混。
如果你打算长期跑编码类 Agent,Coding Plan 会比按次调用更省心;如果只是验证模型能不能正确输出工具调用,先去模型对话页手动试一轮,确认模型认识read_file这个工具名,再进代码。
3. 可复制配置:settings.json 骨架 + 工具定义,一次写对
这一节是全文最需要你动手的部分。我会给出一份可直接复制的settings.json骨架,路径和字段名保持和 Claude Code 风格一致,同时给出工具定义的 JSON Schema。你不需要改结构,只需要替换 Key 和模型名。
先看配置文件。放在项目根目录下的.claude/settings.json(如果你用的是兼容 Claude Code 的客户端,路径按客户端要求来,字段名不变):
{ "api": { "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoToken密钥", "model": "claude-sonnet-4-20250514", "timeout": 60, "max_retries": 2 }, "tools": { "enabled": ["read_file", "write_file"], "workspace": "./test_workspace", "max_read_bytes": 5000, "backup_on_write": true }, "agent": { "max_iterations": 10, "verbose": true } }三个字段要重点核对:base_url必须是https://taotoken.net/api,不要带多余斜杠;api_key从 API Keys 页面拿;model填你在模型对话页确认可用的 Model ID。这三件套(Base URL + Key + Model ID)是后面所有请求的地基,缺一个都会在验证阶段报错。
接下来是工具定义。Claude Code 风格的工具调用依赖模型输出结构化的调用意图,所以工具要用 JSON Schema 描述清楚。下面这份可以直接放进你的工具注册代码里:
[ { "name": "read_file", "description": "读取指定路径的文件内容,返回文本。路径相对于 workspace。", "input_schema": { "type": "object", "properties": { "path": { "type": "string", "description": "文件路径,例如 test_workspace/buggy.py" } }, "required": ["path"] } }, { "name": "write_file", "description": "将内容写入指定路径的文件,覆盖原内容,写入前自动备份。", "input_schema": { "type": "object", "properties": { "path": { "type": "string", "description": "文件路径,例如 test_workspace/fixed.py" }, "content": { "type": "string", "description": "要写入的完整文本内容" } }, "required": ["path", "content"] } } ]注意input_schema这个字段名,Anthropic 风格的 Tool Use 用的是它,不是 OpenAI 的parameters。如果你混用了,模型可能不报错但也不调用工具,排查起来很费时间。
工具函数本身保持“返回字符串、不抛异常”的原则。下面是最小实现,放在tools.py:
import os import json WORKSPACE = "./test_workspace" MAX_READ = 5000 def read_file(path: str) -> str: full = os.path.join(WORKSPACE, path) if not path.startswith(WORKSPACE) else path if not os.path.exists(full): return f"错误:文件不存在 - {path}" if not os.path.isfile(full): return f"错误:{path} 是目录,不是文件" try: with open(full, "r", encoding="utf-8") as f: content = f.read() if len(content) > MAX_READ: return content[:MAX_READ] + f"\n... (已截断,共 {len(content)} 字符)" return content except Exception as e: return f"错误:无法读取 - {str(e)}" def write_file(path: str, content: str) -> str: full = os.path.join(WORKSPACE, path) if not path.startswith(WORKSPACE) else path parent = os.path.dirname(full) if parent and not os.path.exists(parent): return f"错误:父目录不存在 - {parent}" try: if os.path.exists(full): with open(full, "r", encoding="utf-8") as f: old = f.read() with open(full + ".backup", "w", encoding="utf-8") as f: f.write(old) with open(full, "w", encoding="utf-8") as f: f.write(content) return f"成功:已写入 {path}" except Exception as e: return f"错误:无法写入 - {str(e)}"工具分发器负责把模型返回的tool_use块映射到函数。Anthropic 风格下模型返回的是结构化对象,不需要正则解析字符串,这比教学版省心很多:
TOOL_MAP = { "read_file": read_file, "write_file": write_file, } def execute_tool(name: str, args: dict) -> str: fn = TOOL_MAP.get(name) if fn is None: return f"错误:未知工具 - {name}" try: return fn(**args) except TypeError as e: return f"错误:参数不匹配 - {str(e)}"到这里,配置和工具都齐了。下一步是把它接到模型请求上,验证整条链路。
4. 验证请求:一次真实的读写动作,确认工具调用链路已通
验证不要贪多,一次只做一件事:让 Agent 读取一个已知有 Bug 的文件,然后写出修复版本。成功标准有两个——Observation 里出现真实文件内容,且磁盘上真的多了一个修复后的文件。
先准备测试文件:
import os os.makedirs("test_workspace", exist_ok=True) with open("test_workspace/buggy.py", "w", encoding="utf-8") as f: f.write("""def calculate(a, b): # 这里应该是加法 return a - b result = calculate(5, 3) print(f"5 + 3 = {result}") """)然后发一轮带工具的请求。关键是把tools定义和tool_choice一起传进去,模型才会在需要时返回tool_use:
import json import requests cfg = json.load(open(".claude/settings.json")) headers = { "x-api-key": cfg["api"]["api_key"], "anthropic-version": "2023-06-01", "content-type": "application/json", } payload = { "model": cfg["api"]["model"], "max_tokens": 1024, "tools": json.load(open("tools_schema.json")), "messages": [ {"role": "user", "content": "读取 test_workspace/buggy.py,找出 Bug 并把修复后的内容写入 test_workspace/fixed.py"} ], } resp = requests.post( cfg["api"]["base_url"] + "/v1/messages", headers=headers, json=payload, timeout=cfg["api"]["timeout"], ) print(resp.status_code) print(resp.text[:800])如果链路通了,你会看到返回体里出现stop_reason: "tool_use",并且content数组里有一个type: "tool_use"的块,name是read_file,input里带着path。这时候把execute_tool的结果作为tool_result回填,再发第二轮,模型就会继续调用write_file。
回填的格式要严格:
tool_result = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block["id"], "content": execute_tool(tool_use_block["name"], tool_use_block["input"]), } ], }跑完之后检查磁盘:
ls test_workspace/ # buggy.py buggy.py.backup fixed.py cat test_workspace/fixed.py如果fixed.py里return a - b变成了return a + b,说明从模型输出到函数执行再到结果回填的闭环完整跑通了。这一步成功,你的 Agent 才算真正有了“双手”。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
工具调用链路长,报错点分散。下面这几类是我在接入过程中反复遇到的,按现象对照处理。
401 Unauthorized:九成是 Key 或 Base URL 的问题。先确认api_key是从 API Keys 页面复制的完整值,没有多余空格;再确认base_url是https://taotoken.net/api,没有写成带/v1的完整路径导致重复拼接。如果用的是环境变量,检查变量名是否和代码里读取的一致。
local proxy failed:这个报错通常出现在客户端把请求转发到本地代理时。检查settings.json里是否残留了旧的代理地址,或者系统环境变量里有没有指向本地的HTTP_PROXY。把配置收敛到 TaoToken 的 Base URL 之后,这类问题会消失。
reading choices 相关报错:如果你混用了 OpenAI 风格的响应解析,会去读choices[0].message,但 Anthropic 风格返回的是content数组。工具调用场景下要读的是content里type == "tool_use"的块。解析路径写错,就会在“reading choices”这一步空指针。
OAuth 相关报错:部分客户端默认走 OAuth 登录流程,而 API Key 模式不需要。确认你用的是 Key 鉴权而不是登录态鉴权,两者不要混。如果客户端强制 OAuth,去设置里切换到 API Key 模式。
工具没被调用:模型返回了普通文本而不是tool_use。先检查tools字段是否真的传了,再检查input_schema字段名是否正确。如果工具描述太模糊,模型也可能选择不调用,把description写具体一点。
Observation 回填后模型重复调用同一工具:检查tool_use_id是否和上一轮返回的id完全一致。ID 对不上,模型会认为工具没执行,于是重试。
排错时建议把每一轮的stop_reason、tool_use.name、tool_result前 200 字符打出来。链路断在哪一环,日志会直接告诉你。
6. 把链路固定下来:下一步与长期编码建议
读写文件跑通之后,建议你立刻做一件事:把这次成功的请求和响应存成 fixture,后面改代码时用它做回归。工具调用最容易在“看起来没改什么”的时候坏掉,有 fixture 能省很多时间。
如果你打算继续往下做终端工具、上下文管理,模型调用会越来越频繁,这时候按次调用不如用 Coding Plan 划算,配置也只需要把 Key 和 Model ID 换一次。验证模型能力时,模型对话页可以手动试工具调用意图,不用每次都跑代码。
接入文档里有更完整的字段说明,遇到input_schema或tool_result格式不确定的时候,对照文档比猜快。Claude Code 接入说明则覆盖了客户端侧的配置细节,如果你用的是现成客户端而不是自己写请求,从那里入手更直接。
最后留一个实用习惯:每次让 Agent 写文件之前,先git add . && git commit。工具调用再稳,也架不住模型偶尔理解偏。有 Git 兜底,你才敢让它真的动手。