1. 从聊天机器人到 Agent:差的不只是几个工具
很多人第一次接触 Agent 这个概念时,会觉得它和聊天机器人差不多——不就是问一句答一句吗?我一开始也这么想,直到真正动手搭了一个才发现,两者之间隔着一整套「思考-行动-观察」的循环机制。
聊天机器人的工作模式是:你给一段文字,它回一段文字,结束。它不会主动去查资料,不会调用计算器,更不会在发现自己答错后重新规划。而 Agent 的核心区别在于,它能自己决定「下一步该干什么」——是先搜索一下最新数据,还是直接调用某个 API,或者干脆承认信息不足需要追问。这个决策过程,就是 ReAct(Reasoning + Acting)循环要解决的问题。
这篇内容面向的是想从零跑通第一个 Agent 原型的开发者。你不需要有 LangChain 深度使用经验,但最好写过 Python、调过至少一个 LLM 的 API。我会用一个统一的 Key 来打通 LLM 推理和工具调用两条通道,避免在多个平台之间来回切换 Key 和配置。整个链路拆成四块:环境准备、Agent 骨架配置、ReAct 循环实现、一次完整的验证请求。跟着走一遍,你能得到一个能自主决定「要不要调工具、调哪个工具」的最小可用 Agent。
2. 前置准备:用 TaoToken 统一 Key 打通 LLM 与工具通道
搭 Agent 最烦的事情之一,是 LLM 一个 Key、搜索工具一个 Key、代码执行环境又一个 Key,配置散落在四五个地方,调试的时候光找 Key 就耗掉一半耐心。我试过把不同供应商的 Key 写在一个.env里,结果每次换模型都要改代码里的 base_url 和 model 名,非常容易出错。
TaoToken 在这里的作用是提供一个统一的接入层:LLM 推理请求走同一个 API 地址,工具调用通道也通过同一套 Key 体系管理。你只需要在settings.json里维护一份配置,Agent 的推理模块和工具模块都从这里读。
先拿到 Key。访问控制台创建 API Key:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite创建完成后,你会得到一个以sk-开头的字符串。把它存到环境变量里,不要硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的key"如果你用的是 Claude Code 或类似的编码 Agent 工具,可以直接参考接入文档里的配置方式:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewriteAPI 的基础地址是https://taotoken.net/api,注意这个地址不带 UTM 参数,直接用于代码里的base_url配置。官网入口在这里:
https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=3. 可复制配置:Agent 骨架的 settings.json 片段
Agent 的骨架配置需要解决三件事:LLM 怎么调、工具怎么注册、ReAct 循环的提示词模板长什么样。下面这份settings.json可以直接复制使用,把 Key 的部分替换成你自己的。
{ "llm": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model": "claude-sonnet-4-20250514", "max_tokens": 2048, "temperature": 0.3 }, "tools": { "search_web": { "enabled": true, "description": "搜索互联网获取实时信息,输入为搜索关键词字符串", "endpoint": "https://taotoken.net/api/tools/search", "api_key_env": "TAOTOKEN_API_KEY" }, "run_python": { "enabled": true, "description": "执行 Python 代码片段,输入为可运行的代码字符串", "endpoint": "https://taotoken.net/api/tools/code", "api_key_env": "TAOTOKEN_API_KEY" } }, "agent": { "max_iterations": 6, "stop_sequence": "Final Answer:", "prompt_template": "react_v1" } }几个关键参数说明一下。max_iterations控制 ReAct 循环最多跑几轮,设成 6 是防止 Agent 陷入无限思考——我踩过的坑就是没设上限,结果模型在「搜索-发现不够-再搜索」之间循环了十几次,Token 消耗直接起飞。temperature设 0.3 是因为 Agent 需要稳定的决策,太高的随机性会让它频繁选错工具。stop_sequence用来告诉模型什么时候该输出最终答案而不是继续调工具。
工具注册部分,每个工具需要提供description,这段文字会直接拼进 Prompt 里,模型靠它来判断该不该调用这个工具。描述写得越清楚,工具选择越准确。比如「搜索互联网获取实时信息」就比「搜索工具」好得多。
Prompt 模板单独放在一个文件里,核心结构是这样的:
REACT_PROMPT = """你是一个可以使用工具的智能助手。请严格按照以下格式回应: Question: 用户的问题 Thought: 你需要思考当前该做什么 Action: 要调用的工具名,必须是 [{tool_names}] 中的一个 Action Input: 传给工具的输入 Observation: 工具返回的结果 ...(Thought/Action/Action Input/Observation 可以重复多次) Thought: 我现在知道最终答案了 Final Answer: 对用户问题的最终回答 可用工具: {tools} 开始: Question: {input} {agent_scratchpad}"""agent_scratchpad是循环过程中不断累积的中间步骤,每次调用 LLM 时把之前的 Thought-Action-Observation 记录拼进去,模型就能基于历史决定下一步。
4. 实现 ReAct 循环:从 Thought 到 Observation 的完整代码
配置就绪后,核心逻辑就是一个 while 循环。下面这段代码实现了完整的 ReAct 流程,可以直接跑:
import os import json import re import requests with open("settings.json") as f: config = json.load(f) API_KEY = os.environ[config["llm"]["api_key_env"]] BASE_URL = config["llm"]["base_url"] def call_llm(prompt: str) -> str: resp = requests.post( f"{BASE_URL}/v1/messages", headers={ "x-api-key": API_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json" }, json={ "model": config["llm"]["model"], "max_tokens": config["llm"]["max_tokens"], "temperature": config["llm"]["temperature"], "messages": [{"role": "user", "content": prompt}] } ) resp.raise_for_status() return resp.json()["content"][0]["text"] def execute_tool(tool_name: str, tool_input: str) -> str: tool_cfg = config["tools"].get(tool_name) if not tool_cfg or not tool_cfg["enabled"]: return f"错误:工具 {tool_name} 未注册或未启用" resp = requests.post( tool_cfg["endpoint"], headers={"Authorization": f"Bearer {API_KEY}"}, json={"input": tool_input} ) resp.raise_for_status() return resp.json().get("output", "工具返回为空") def run_agent(user_input: str) -> str: tool_names = [k for k, v in config["tools"].items() if v["enabled"]] tool_descs = "\n".join( f"- {k}: {v['description']}" for k, v in config["tools"].items() if v["enabled"] ) scratchpad = "" for i in range(config["agent"]["max_iterations"]): prompt = REACT_PROMPT.format( tools=tool_descs, tool_names=", ".join(tool_names), input=user_input, agent_scratchpad=scratchpad ) output = call_llm(prompt) if "Final Answer:" in output: return output.split("Final Answer:")[-1].strip() action_match = re.search(r"Action:\s*(\w+)", output) input_match = re.search(r"Action Input:\s*(.+)", output) if not action_match or not input_match: scratchpad += output + "\nObservation: 格式错误,请按 Thought/Action/Action Input 格式输出\n" continue tool_name = action_match.group(1).strip() tool_input = input_match.group(1).strip() observation = execute_tool(tool_name, tool_input) scratchpad += f"{output}\nObservation: {observation}\n" return "达到最大迭代次数,未能得出最终答案"这段代码的关键点在于scratchpad的累积方式。每次循环把模型输出的 Thought 和 Action、以及工具返回的 Observation 拼接到一起,下一轮再喂回去。模型看到「我之前搜了什么、得到了什么结果」,就能判断是继续调工具还是给出最终答案。
execute_tool里做了工具名的校验,如果模型输出了一个不存在的工具名,会返回错误信息而不是直接崩溃。这个错误信息也会进入 Observation,模型看到后通常会修正自己的选择。
5. 验证请求:一次完整的 Thought-Action-Observation 流程
配置和代码都就位后,跑一个真实请求来验证整条链路。用下面这个调用:
result = run_agent("帮我查一下 2025 年诺贝尔物理学奖颁给了谁,并计算获奖者人数乘以 100 万") print(result)预期会看到类似这样的中间过程(实际输出取决于模型和工具返回):
Thought: 这个问题需要两步:先搜索诺贝尔物理学奖信息,再做乘法计算。 Action: search_web Action Input: 2025 诺贝尔物理学奖 获奖者 Observation: 2025 年诺贝尔物理学奖授予 John Clarke、Michel Devoret 和 John Martinis... Thought: 我找到了 3 位获奖者,现在需要计算 3 * 1000000。 Action: run_python Action Input: print(3 * 1000000) Observation: 3000000 Thought: 我现在知道最终答案了。 Final Answer: 2025 年诺贝尔物理学奖授予 3 位科学家,获奖者人数乘以 100 万等于 3000000。这个流程完整展示了 ReAct 的三个阶段:Thought 是模型的推理,Action 是它选择的工具,Observation 是工具返回的结果。模型在第一轮判断需要搜索,第二轮判断需要计算,第三轮确认信息足够后输出最终答案。
如果你想单独验证模型对话通道是否正常,可以用模型对话入口快速测一下:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite如果 Agent 需要长期运行、频繁调用工具,建议看一下 Coding Plan 的额度方案:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite6. 本篇常见错排查
报错一:KeyError: 'TAOTOKEN_API_KEY'
环境变量没设置或者设置在了错误的 shell 会话里。检查echo $TAOTOKEN_API_KEY是否有输出。如果是在 IDE 里运行,需要在运行配置里单独加环境变量,而不是只在终端 export。
报错二:模型一直输出 Action 但不输出 Final Answer
通常是 Prompt 模板里的stop_sequence没生效,或者max_iterations设得太小。先检查模板里是否明确写了「如果信息足够,输出 Final Answer」。另一个可能是工具返回的 Observation 太长,把上下文撑满了,模型看不到完整历史。可以在execute_tool里对返回结果做截断,比如只保留前 500 字符。
报错三:工具调用返回 401
工具通道的 Key 和 LLM 通道的 Key 不一致。检查settings.json里工具的api_key_env是否指向了同一个环境变量。如果工具端点是外部服务,确认该服务的鉴权方式是不是 Bearer Token。
报错四:ReAct 循环卡在同一个工具上反复调用
模型没有正确解析 Observation。检查execute_tool返回的内容是否包含换行符或特殊字符,这些可能干扰模型对格式的解析。建议在 Observation 前后加明确的分隔标记,比如[OBSERVATION_START]...[OBSERVATION_END]。
报错五:requests.exceptions.SSLError
本地 Python 环境的证书链有问题。可以临时用verify=False跳过验证来确认是否是证书问题,但生产环境不要这么做。正确的做法是更新certifi包:pip install --upgrade certifi。
排查完这些之后,如果还有接入层面的问题,可以对照接入文档里的示例请求逐项检查:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite7. 下一步:从原型到可用 Agent 的迭代方向
跑通上面这个最小原型后,你手里已经有一个能自主决策的 Agent 了。接下来可以按需加东西:加一个向量数据库做长期记忆,让 Agent 记住跨会话的信息;把工具从两个扩展到五六个,覆盖搜索、计算、文件读写、API 调用;优化 Prompt 模板,加入 Few-shot 示例来提升工具选择的准确率。
但别一上来就堆功能。我的建议是先把当前这个版本跑稳,用十几个不同的问题测一遍,观察它在哪些情况下会选错工具、哪些情况下会陷入循环。把这些边界情况摸清楚之后,再针对性地加工具和改 Prompt,比盲目扩展要有效得多。