1. 为什么你的 Agent 演示能跑,上线就崩
如果你正在做 LLM 应用,大概率遇到过这个场景:本地写了个 ReAct 循环,接了三五个工具,演示时丝滑流畅。一旦放到真实环境里跑长任务,模型三步之后就忘了自己干了什么,工具调用悄无声息地失败,上下文窗口里塞满了没用的工具输出,最后整个链路卡死。
问题不在模型本身。问题在模型周围那一层——现在有个专门的叫法:Agent Harness。它指的是包裹 LLM 的完整软件基础设施:编排循环、工具注册与执行、记忆管理、上下文压缩、状态持久化、错误恢复、权限防护。Anthropic 的文档里直接把 Claude Code 的 SDK 称为“驱动 Claude Code 的 Agent Harness”,OpenAI 的 Codex 团队也把 Agent 和 Harness 等同看待。
我试过把一个 ReAct Agent 从脚本改成可配置的 Harness 骨架,最大的感受是:模型能力决定上限,Harness 决定你能不能稳定摸到那个上限。一个 10 步的流程,每步 99% 成功率,端到端只有约 90.4%——错误会快速累积,而 Harness 就是用来兜住这些错误的。
这篇要交付的是一套可复制的settings.json与config.toml骨架,把 ReAct 推理循环和 LangChain 工具调用统一到同一个 Key/API 通道上,让你能快速跑通 Agent 工具链,并且知道每一步为什么这么配。
2. 前置准备:统一 Key 与 API 通道
在写配置文件之前,先把“模型从哪来”这件事定下来。Agent Harness 最怕的就是工具调用到一半,API 通道换了、Key 失效了、返回格式不一致了。所以第一步是把模型访问收敛到一个稳定的入口。
TaoToken 在这里扮演的角色就是统一通道:你拿一个 Key,通过一个兼容 OpenAI 风格的 API 地址,就能访问多种模型,ReAct 循环里的每一次 LLM 调用、LangChain 的每一次工具绑定,都走同一个 base_url。这样 Harness 的配置骨架只需要维护一份凭证,不用为每个模型单独写适配层。
具体操作路径:
- 打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册并登录
- 进入控制台 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 创建一个新 Key,复制保存
- API 基础地址统一用 https://taotoken.net/api(这个地址不加 UTM 参数)
注意:Key 只显示一次,建议创建后立刻写进环境变量或本地配置文件,不要硬编码在会提交到 git 的代码里。
拿到 Key 之后,先别急着写 Agent,用一条最简单的请求确认通道是通的。这一步很重要,因为后面所有排障都要先排除“通道本身不通”这个可能。
export TAOTOKEN_API_KEY="sk-你的Key" curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "只回复两个字:通了"}] }'如果返回里能看到choices[0].message.content是“通了”,说明 Key 和通道都没问题。这一步过了,再往下配 Harness 才有意义。
3. settings.json 骨架:ReAct 循环与工具注册
现在进入核心部分。Agent Harness 的配置骨架要解决三件事:模型怎么调、工具怎么注册、循环怎么控制。下面这份settings.json是一个可以直接改改就用的骨架,我把它拆成几个区块来讲。
{ "harness": { "name": "react-agent-harness", "version": "1.0.0", "max_turns": 12, "max_tokens_budget": 120000, "loop": { "type": "react", "thought_action_observation": true, "stop_on_no_tool_call": true, "parallel_readonly_tools": true } }, "llm": { "provider": "openai-compatible", "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model": "gpt-4o-mini", "temperature": 0.2, "timeout_seconds": 60, "max_retries": 2 }, "tools": { "registry": [ { "name": "read_file", "description": "读取指定路径的文件内容", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "文件路径" } }, "required": ["path"] }, "readonly": true, "timeout_seconds": 10 }, { "name": "run_shell", "description": "在沙箱中执行 shell 命令", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "要执行的命令" } }, "required": ["command"] }, "readonly": false, "requires_confirmation": true, "timeout_seconds": 30 } ] }, "context": { "compression_threshold": 0.75, "keep_recent_turns": 6, "mask_old_tool_output": true, "max_tool_output_chars": 4000 }, "memory": { "short_term": "conversation_history", "long_term_file": "./MEMORY.md", "index_file": "./MEMORY_INDEX.md" }, "guardrails": { "input_check": true, "output_check": true, "tool_permission_check": true, "blocked_commands": ["rm -rf /", "shutdown", "reboot"] } }几个关键点解释一下。
loop.type设为react,对应的是思想-行动-观察(TAO)循环:组装提示、调用 LLM、解析输出、执行工具、把结果喂回去、重复。max_turns是硬性回合上限,防止模型陷入死循环。parallel_readonly_tools打开后,只读工具可以并发执行,变更类工具串行执行,这是生产级 Harness 的常见做法。
llm.base_url指向https://taotoken.net/api,api_key_env指定从环境变量读取 Key,这样配置文件本身可以安全地提交到仓库。max_retries设为 2,对应 Stripe 生产 Harness 的经验值——重试次数上限两次,再多就是浪费。
tools.registry里每个工具都有readonly和requires_confirmation两个字段。只读工具自动放行,变更类工具需要确认。这就是权限执行和模型推理分离的思路:模型决定尝试什么,工具系统决定允许什么。
context.compression_threshold设为 0.75,意思是上下文用到 75% 时触发压缩。mask_old_tool_output打开后,旧的工具输出会被隐藏但保留工具调用记录,这是 JetBrains Junie 用过的观察掩码策略。
memory区块里,短期记忆就是对话历史,长期记忆落到MEMORY.md文件,索引文件保持轻量。Claude Code 的三层记忆设计就是这个思路:轻量索引始终加载,详细主题按需拉取,原始转录只通过搜索访问。
4. config.toml:LangChain 工具调用接入
如果你用的是 LangChain 或 LangGraph,可以把上面的骨架映射成config.toml,让 LangChain 的工具调用走同一套通道。LangGraph 把 Harness 建模为显式状态图,两个节点llm_call和tool_node通过条件边连接:有工具调用就路由到tool_node,没有就路由到END。
[harness] name = "langchain-react-harness" max_turns = 12 max_tokens_budget = 120000 [llm] provider = "openai-compatible" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model = "gpt-4o-mini" temperature = 0.2 timeout_seconds = 60 max_retries = 2 [graph] entry_point = "llm_call" recursion_limit = 25 [graph.nodes.llm_call] type = "llm" bind_tools = true tool_choice = "auto" [graph.nodes.tool_node] type = "tool_executor" parallel_readonly = true max_concurrency = 4 [graph.edges] llm_call_to_tool = "has_tool_calls" llm_call_to_end = "no_tool_calls" tool_to_llm = "always" [context] compression_threshold = 0.75 keep_recent_turns = 6 mask_old_tool_output = true max_tool_output_chars = 4000 [guardrails] input_check = true output_check = true tool_permission_check = true对应的 Python 侧接入代码大致长这样,重点是base_url和api_key都从配置读,不写死:
import os import json from langchain_openai import ChatOpenAI from langchain_core.tools import tool with open("settings.json", "r", encoding="utf-8") as f: cfg = json.load(f) llm = ChatOpenAI( model=cfg["llm"]["model"], base_url=cfg["llm"]["base_url"], api_key=os.environ[cfg["llm"]["api_key_env"]], temperature=cfg["llm"]["temperature"], timeout=cfg["llm"]["timeout_seconds"], max_retries=cfg["llm"]["max_retries"], ) @tool def read_file(path: str) -> str: """读取指定路径的文件内容""" with open(path, "r", encoding="utf-8") as f: return f.read()[: cfg["context"]["max_tool_output_chars"]] @tool def run_shell(command: str) -> str: """在沙箱中执行 shell 命令""" import subprocess result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=30 ) return (result.stdout + result.stderr)[: cfg["context"]["max_tool_output_chars"]] tools = [read_file, run_shell] llm_with_tools = llm.bind_tools(tools)这里有个容易踩的坑:bind_tools之后,模型返回的是结构化的tool_calls对象,不是自由文本。Harness 的输出解析层要检查“有工具调用吗”,有就执行并循环,没有就是最终答案。不要再去写正则解析文本,那是遗留做法。
5. 连通性验证:跑通第一个 ReAct 回合
配置写完了,怎么确认整条链路是通的?分三步验证。
第一步,验证 LLM 通道。用第 2 节的 curl 命令确认能拿到回复。
第二步,验证工具绑定。跑一段最小代码,看模型是否会主动发起工具调用:
from langchain_core.messages import HumanMessage messages = [HumanMessage(content="读取 ./README.md 的前 200 个字符")] response = llm_with_tools.invoke(messages) print("是否有工具调用:", bool(response.tool_calls)) if response.tool_calls: for call in response.tool_calls: print("工具名:", call["name"]) print("参数:", call["args"])如果输出里是否有工具调用: True,并且工具名是read_file,说明模型正确理解了你注册的工具模式。
第三步,验证完整 ReAct 循环。用 LangGraph 把两个节点连起来跑:
from langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolNode from typing import TypedDict, Annotated import operator class AgentState(TypedDict): messages: Annotated[list, operator.add] def llm_call(state: AgentState): return {"messages": [llm_with_tools.invoke(state["messages"])]} def should_continue(state: AgentState): last = state["messages"][-1] if getattr(last, "tool_calls", None): return "tool_node" return END graph = StateGraph(AgentState) graph.add_node("llm_call", llm_call) graph.add_node("tool_node", ToolNode(tools)) graph.set_entry_point("llm_call") graph.add_conditional_edges("llm_call", should_continue, {"tool_node": "tool_node", END: END}) graph.add_edge("tool_node", "llm_call") app = graph.compile() result = app.invoke( {"messages": [HumanMessage(content="读取 ./README.md 并告诉我文件有多少行")]}, config={"recursion_limit": 25}, ) print(result["messages"][-1].content)跑通后你会看到模型先调用read_file,拿到内容后再生成最终回答。这就是一个完整的 ReAct 回合:组装提示、调用 LLM、解析工具调用、执行工具、结果回喂、生成答案。
6. 本篇常见错误排查
配置和验证过程中,最容易卡在这几个地方。
报错一:401 Unauthorized。大概率是 Key 没读到。检查TAOTOKEN_API_KEY环境变量是否在当前 shell 生效,echo $TAOTOKEN_API_KEY看有没有值。如果是 Python 里读的,确认os.environ能取到,别在 IDE 里配了环境变量但终端没配。
报错二:模型不调用工具,直接编答案。检查工具描述是否清晰。工具描述是模型判断“什么时候用哪个工具”的唯一依据,写得太模糊模型就自己编。另外确认tool_choice是auto而不是none。
报错三:工具调用参数解析失败。检查parameters的 JSON Schema 是否合法,required字段和properties是否对得上。参数类型写错会导致模型生成的参数无法通过校验。
报错四:循环停不下来,一直调用工具。检查max_turns和recursion_limit是否生效。另外看工具返回结果是不是空字符串——如果工具一直返回空,模型会反复重试。给工具输出加长度截断和明确的“无结果”提示。
报错五:上下文超限。检查compression_threshold是否触发。如果工具输出特别长,先把max_tool_output_chars调小,比如从 4000 降到 2000,再观察。上下文腐烂是真实存在的:关键内容掉进窗口中间位置时,模型性能下降可能超过 30%。
报错六:并发工具执行时状态错乱。确认只读工具才开并发,变更类工具必须串行。parallel_readonly_tools和parallel_readonly这两个开关不要对变更类工具打开。
排障时如果怀疑是通道问题,回到第 2 节的 curl 命令重新验证一次。如果怀疑是模型能力问题,可以去模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 直接对话测试同一个 prompt,对比 Harness 里的表现。
7. 把 Harness 当成产品来迭代
跑通第一个 ReAct 回合只是开始。真正决定 Agent 能不能上生产的,是 Harness 的迭代方式。这里给几个实操建议。
第一,先最大化单个 Agent。Anthropic 和 OpenAI 都建议,只有当工具数量超过约 10 个且明显重叠,或者任务域清晰分离时,才拆多 Agent。多 Agent 会增加路由的额外 LLM 调用和交接时的上下文丢失。
第二,工具不是越多越好。Vercel 从 v0 移除了 80% 的工具后结果反而更好。原则是暴露当前步骤所需的最小工具集,其余用延迟加载。
第三,验证循环是分水岭。给模型一种验证自己工作的方式,质量能提升 2 到 3 倍。计算验证(测试、linter)提供确定性基础真值,推理验证(LLM 作为评委)捕捉语义问题。两者结合用。
第四,Harness 要能变薄。随着模型改进,很多规划步骤会被模型内化,Harness 里对应的逻辑就该删掉。Anthropic 定期从 Claude Code 的 Harness 中删除规划步骤,就是因为新模型版本已经能自己做这件事。如果你的 Harness 越改越厚,可能方向反了。
如果你打算长期做编码类 Agent 或者多轮工具链,可以了解下 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite,它更适合需要稳定通道和额度管理的持续开发场景。接入细节和参数说明可以查接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有完整的接口对照。
下次 Agent 失败的时候,先别怪模型。打开你的settings.json,看看 Harness 这一层是不是哪里漏了。