1. 从一次「工具没被调用」说起:Agent 交互链路到底卡在哪
如果你在本地调试过 Agent,大概率遇到过这种场景:模型嘴上说「我来查一下天气」,但工具调用压根没发出去;或者 MCP Server 明明注册成功了,tools/list也返回了工具,模型却始终用自然语言回答,不吐<use_mcp_tool>。问题往往不在模型本身,而在 Agent 与 LLM 之间的交互链路——ReAct 循环有没有闭合、MCP 工具描述有没有进到 system prompt、工具返回有没有按协议回灌。
这篇聚焦 Agent 与 LLM 交互链路中的 ReAct 循环与 MCP 工具调用,面向本地调试场景。我会给出config.toml与settings.json的可复制骨架,并演示用统一 Key/API 通道(TaoToken,https://taotoken.net/?utm_source=taotoken_aicg_blog_end)完成一次 Prompt 到工具返回的端到端验证动作。适合已经在用 Cline、Cursor 或自研 Agent 框架,但想搞清楚「消息到底怎么流转」的开发者。
ReAct 的核心其实就一句话:让模型在决定 action 之前先输出 thought,再把 thought、action、observation 一起塞回下一轮 prompt。MCP 则负责把「有哪些工具、参数长什么样」用结构化 JSON 告诉模型。两者拼起来,才是一个能持续思考、持续调工具的 Agent。下面从配置骨架开始拆。
2. TaoToken 前置:统一 Key 与 API 通道准备
本地调试 Agent 最烦的是模型接口换来换去,OpenAI 格式、Anthropic 格式、各家 base_url 都不一样。我习惯用一个兼容 OpenAI 规范的统一通道,把 base_url 和 Key 固定下来,Agent 侧只认一套配置。TaoToken 就是干这个的:官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,API 入口 https://taotoken.net/api 。
你需要先拿到一个 API Key。登录后进控制台,在 API Keys 页面创建一个,复制出来形如sk-xxxx。这个 Key 后面会写进settings.json的apiKey字段,以及config.toml里模型 provider 的api_key。
注意:Key 只存在本地配置文件里,别提交到 git。建议把
settings.json和config.toml加进.gitignore。
模型选择上,调试 ReAct 循环建议用带 thinking 能力的模型,因为工具调用格式的遵循度更稳。你可以在模型对话页面先手动测一下模型能不能按 XML 格式吐工具调用,确认没问题再接到 Agent 里。模型对话入口:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面写了 base_url 拼接规则和 OpenAI 兼容端点。简单说,chat completions 的完整地址就是https://taotoken.net/api/v1/chat/completions,和 OpenAI 官方格式一致,Agent 侧不用改解析逻辑。
3. 可复制配置:config.toml 与 settings.json 骨架
先给config.toml,这是 Agent 主程序的配置,管模型 provider 和 MCP Server 注册。我把它放在项目根目录,用toml解析。
# config.toml —— Agent 主配置 [llm] provider = "openai_compatible" base_url = "https://taotoken.net/api/v1" api_key = "sk-你的Key" model = "你的模型名" temperature = 0.0 max_tokens = 4096 stream = true [agent] max_iterations = 12 # ReAct 循环上限,防止死循环 tool_call_format = "xml" # 工具调用格式:xml / json require_thinking = true # 强制模型先输出 thinking working_dir = "/Users/you/Code/agent-demo" [mcp.servers.weather] type = "stdio" command = "uv" args = [ "--directory", "/Users/you/Code/agent-demo/weather", "run", "weather.py" ] enabled = true关键点:max_iterations一定要设,否则模型可能反复调同一个工具。require_thinking打开后,Agent 在拼 prompt 时会显式要求模型先输出<thinking>块,这是 ReAct 的 thought 部分。
再给settings.json,这是给编辑器插件(Cline 类)用的 MCP 注册配置,放在.vscode或插件指定目录。
{ "mcpServers": { "weather": { "type": "stdio", "command": "uv", "args": [ "--directory", "/Users/you/Code/agent-demo/weather", "run", "weather.py" ], "env": { "MCP_LOG_LEVEL": "debug" } } }, "llm": { "baseUrl": "https://taotoken.net/api/v1", "apiKey": "sk-你的Key", "model": "你的模型名" } }两个文件的分工要清楚:config.toml管 Agent 主循环和模型通道,settings.json管 MCP Server 的进程启动方式。command和args决定了 MCP Server 怎么被拉起——这里是 stdio 传输,Agent 会 fork 一个子进程,通过标准输入输出收发 JSON-RPC 消息。
提示:
--directory后面必须是绝对路径。相对路径在 stdio 模式下经常因为工作目录不对而找不到weather.py,这是新手最常见的坑之一。
4. 验证请求:从 Prompt 到工具返回的端到端跑通
配置写好后,先别急着接 Agent,手动验证一遍 MCP Server 能不能正常握手。用uv run直接启动,然后往 stdin 里喂一条initialize请求。
cd /Users/you/Code/agent-demo/weather uv run weather.py启动后手动输入初始化消息(JSON-RPC 2.0):
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"local-debug","version":"0.1.0"}}}正常会返回 serverInfo 和 capabilities,里面能看到tools字段。接着发tools/list:
{"jsonrpc":"2.0","id":1,"method":"tools/list"}返回里应该列出get_alerts和get_forecast两个工具,每个都带inputSchema。这一步通了,说明 MCP Server 侧没问题,问题只可能在 Agent 拼 prompt 的环节。
接下来验证 LLM 通道。用 curl 直接打 TaoToken 的 chat completions,确认 Key 和 base_url 对:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "你的模型名", "messages": [ {"role": "system", "content": "You are an agent. Use tools in XML format."}, {"role": "user", "content": "查询纽约天气"} ], "temperature": 0 }'如果返回正常,说明通道 OK。现在把两边接起来:Agent 在拼 prompt 时,要把tools/list拿到的工具描述塞进 system prompt,格式类似:
## Available Tools - get_forecast: 获取指定经纬度的天气预报 Input Schema: {"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}},"required":["latitude","longitude"]}模型看到这段后,才会在 thought 里决定调用get_forecast,并吐出:
<thinking> 用户要纽约天气,我用 weather 的 get_forecast,纽约坐标 40.7128, -74.0060。 </thinking> <use_mcp_tool> <server_name>weather</server_name> <tool_name>get_forecast</tool_name> <arguments> {"latitude": 40.7128, "longitude": -74.0060} </arguments> </use_mcp_tool>Agent 解析出这段 XML,转成 MCP 的tools/call请求发给 Server:
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_forecast","arguments":{"latitude":40.7128,"longitude":-74.006}}}Server 返回result.content里的文本,Agent 再把它作为 observation 拼回下一轮 prompt。到这里,一个完整的 ReAct 循环就闭合了:thought → action → observation → 下一轮 thought。实测下来,只要工具描述进对了 system prompt,模型调用成功率会高很多。
5. 本篇常见错排查
工具注册了但模型不调用。九成是工具描述没进 system prompt,或者进了但格式不对。检查 Agent 拼 prompt 的代码,确认tools/list的结果被序列化进去了。另一个可能是模型本身工具遵循度差,换个带 thinking 的模型试试。
tools/call返回 method not found。说明 MCP Server 没实现这个工具,或者工具名拼错了。用tools/list的输出核对一遍名字,大小写敏感。
stdio 子进程启动失败。看command和args的路径。uv不在 PATH 里、--directory用了相对路径、weather.py依赖没装,都会导致进程起不来。先在终端手动跑一遍uv run weather.py,能起来再写进配置。
ReAct 循环停不下来。模型反复调同一个工具,通常是 observation 没回灌,或者回灌了但模型没看到。检查消息历史里有没有把工具返回作为role: user或role: tool的消息加进去。max_iterations是兜底,别依赖它。
Key 报 401。检查Authorization头是不是Bearer sk-xxx,base_url 是不是https://taotoken.net/api/v1。少写/v1或写成别的路径都会 404。API Keys 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。
上下文爆炸。Cline 类 Agent 的 system prompt 动辄几万字符,加上历史 thinking 和工具返回,token 涨得飞快。调试时把max_iterations调小,或者只保留最近几轮 observation。
6. 继续往下走:把链路固定成可复用骨架
跑通一次端到端之后,建议把 Agent 的三个模块拆清楚:MCP 模块只管发现、注册、请求/响应解析;LLM 模块只管上下文记录、拼 prompt、解析 XML 响应;交互模块只管展示和终端/文件操作。三者通过消息队列或回调解耦,调试时能单独打日志。
如果你要长期跑编码类 Agent,反复手动配 Key 和 base_url 很烦,可以用 Coding Plan 把模型通道固定下来,Agent 侧只认一套配置:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入细节和 OpenAI 兼容端点说明在文档里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
最后留一个我踩过的坑:调试 MCP 时,先别接真模型,用固定的 mock 响应把 Agent 的解析逻辑跑通,确认 XML 能正确转成tools/call、返回能正确回灌,再接模型。这样出问题时能快速定位是协议层还是模型层,省掉大量来回试的时间。