1. 从一次 Function Calling 到能跑一整天的 Agent Loop你可能已经写过这样的代码给模型几个工具定义用户问一句模型返回一个tool_calls你执行完把结果塞回去模型再吐一段总结。跑通那一刻挺爽但很快会发现它只能回答“今天北京天气怎么样”这种一问一答的事。一旦任务变成“帮我把这个仓库里所有用了旧版 API 的地方改掉并跑通测试”单次调用就彻底不够用了。问题不在于模型不够聪明而在于模型输出代表的东西变了。单次请求里模型输出是一段文本或一个结构化结果系统要管的是输入输出格式、超时和基本审计。到了 Function Calling模型输出变成一个“外部行动请求”你得管工具选择、参数校验、执行和错误回传。再往前一步进入 Agent Loop模型输出变成“下一步该做什么”的决策系统要管的东西一下子膨胀成循环、观察、重试、停止条件和预算。这就是 Harness Engineering 要解决的核心问题当模型从“生成文字”走向“触发行动”模型外面那一圈控制系统的责任就变了。Harness 这个词来自传统软件工程里的 test harness它不是被测对象本身而是给被测对象提供输入、运行环境、观测和断言的外部装置。放到 Agent 场景里Harness 就是紧贴在模型外面的那一层把模型变成能在真实环境里持续行动、同时受到约束和验证的系统。ReAct Loop 是这条路的起点。Yao 等人在 ICLR 2023 提出的 ReAct让模型把推理和行动交错生成推理用来规划、跟踪和处理异常行动用来和外部环境交换信息。这条“推理—行动—观察”的回路就是 Agent Loop 的骨架。但 ReAct Loop 本身是原子节点它一步一步推进对单个任务或简单循环有帮助遇到需要拆解和编排的复杂问题就不够用了。实测下来ReAct 能覆盖大概七八成的常规问题剩下的复杂任务就得靠更完整的 Agent Loop 加上 Harness 控制层来兜。这篇会带你从零搭一个可观测的 Agent Loop用 ReAct 做骨架把 Function Calling 从单次调用升级成能循环推进的系统。我会给出可复制的配置片段、验证清单以及用统一 API 通道接入的方式。适合已经写过 Function Calling、想往 Agent 系统方向落地的开发者。2. TaoToken 统一 Key 与 API 通道的前置准备在动手写 Agent Loop 之前得先把模型调用这条链路理顺。Agent 系统和单次调用最大的区别是调用频次和模型切换需求。一个 ReAct Loop 跑一轮任务可能产生十几次甚至几十次模型请求中间还可能因为任务类型不同需要切换模型——规划用推理强的执行用速度快的总结用便宜的。如果每个模型都单独配一套 Key 和 Base URL代码里会到处是分支判断维护起来很痛苦。我的做法是先用一个统一的 API 通道把模型调用收敛掉。TaoToken 提供的就是这样一个入口官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。它的价值在于你只需要维护一套 Key就能在 Agent Loop 里按需切换不同模型而不用改底层请求逻辑。具体操作上先去控制台创建一个 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 进去之后在 API Keys 页面生成一个 Key复制保存好。这个 Key 就是你后面所有模型调用的凭证。这里要强调一个概念Base URL Key Model ID 这三件套必须配套出现。很多接入失败不是因为代码写错而是这三者没对齐。Base URL 指向 https://taotoken.net/api Key 用你刚生成的那串Model ID 用平台支持的模型标识。三者缺一或者写错就会在请求阶段直接报错。如果你用的是 Claude Code 这类工具接入方式略有不同。Claude Code 需要配置 Anthropic 兼容的端点文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有说明。核心还是那三件套只是配置文件的字段名不一样。对于长期跑编码 Agent 的场景可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合高频、长时间的 Agent 任务。前置准备做完你应该手上有一个可用的 API Key、确认过的 Base URL、以及打算在 Agent Loop 里使用的 Model ID 列表。接下来进入配置环节。3. 可复制的 Agent Loop 配置与 ReAct 骨架这一节给出可以直接抄的配置片段。我按 Python 项目来写因为 Agent Loop 的调试和可观测性在 Python 生态里最顺手。先建一个配置文件把模型通道和 Loop 参数都收进去。{ llm: { base_url: https://taotoken.net/api, api_key: sk-your-key-here, models: { planner: claude-sonnet-4-20250514, executor: claude-3-5-haiku-20241022, summarizer: claude-3-5-haiku-20241022 }, timeout_seconds: 60, max_retries: 3 }, agent_loop: { max_steps: 25, max_tokens_budget: 200000, no_progress_threshold: 3, stop_conditions: [task_complete, budget_exhausted, no_progress], observation_truncate_chars: 4000 }, tools: { enabled: [read_file, write_file, run_shell, search_code], sandbox_root: ./workspace, shell_timeout_seconds: 30 } }这个配置里几个参数值得说明。max_steps是硬性步数上限防止 Loop 无限跑下去。max_tokens_budget是 token 预算超过就强制停止。no_progress_threshold是连续多少步没有实质进展就判定卡住这个在 ReAct Loop 里特别重要因为模型很容易在局部反馈里重复同一策略。observation_truncate_chars控制工具返回结果塞回上下文时的截断长度不截断的话外部结果很容易把上下文淹没。接下来是 ReAct 骨架的核心循环。我用伪代码加真实结构来写你可以直接改成可运行版本。import json import time from typing import Any class AgentLoop: def __init__(self, config: dict, llm_client: Any, tools: dict): self.cfg config[agent_loop] self.llm llm_client self.tools tools self.history [] self.step_count 0 self.token_used 0 self.no_progress_count 0 self.last_observation_hash None def run(self, goal: str) - dict: self.history.append({role: user, content: goal}) while self.step_count self.cfg[max_steps]: self.step_count 1 decision self._think() if decision[type] final: return {status: task_complete, answer: decision[content]} observation self._act(decision) self._observe(observation) if self._should_stop(): break return {status: budget_exhausted, history: self.history} def _think(self) - dict: prompt self._build_react_prompt() resp self.llm.chat( modelself.cfg.get(planner_model), messagesprompt, toolsself._tool_schemas(), ) self.token_used resp.usage.total_tokens return self._parse_decision(resp) def _act(self, decision: dict) - str: tool_name decision[tool] args decision[args] if tool_name not in self.tools: return fERROR: unknown tool {tool_name} try: return self.tools[tool_name](**args) except Exception as e: return fERROR: {type(e).__name__}: {e} def _observe(self, observation: str) - None: truncated observation[: self.cfg[observation_truncate_chars]] self.history.append({role: tool, content: truncated}) h hash(truncated) if h self.last_observation_hash: self.no_progress_count 1 else: self.no_progress_count 0 self.last_observation_hash h def _should_stop(self) - bool: if self.token_used self.cfg[max_tokens_budget]: return True if self.no_progress_count self.cfg[no_progress_threshold]: return True return False这段代码里_think负责推理_act负责行动_observe负责把观察结果写回历史并检测是否卡住。这就是 ReAct Loop 的最小闭环。注意_observe里的 no-progress 检测如果连续几步观察结果哈希一样说明模型在原地打转这时候应该触发重新规划或者直接停止而不是继续烧 token。工具定义部分每个工具都要有清晰的 Schema。工具语义不清、功能重叠是 Agent 选错工具的主要原因。TOOL_SCHEMAS [ { name: read_file, description: 读取指定路径的文件内容返回文本。路径相对于 workspace 根目录。, input_schema: { type: object, properties: { path: {type: string, description: 相对路径如 src/main.py} }, required: [path], }, }, { name: run_shell, description: 在 sandbox 内执行 shell 命令返回 stdout 和 stderr。命令超时 30 秒。, input_schema: { type: object, properties: { command: {type: string, description: 要执行的命令} }, required: [command], }, }, ]配置和骨架都齐了。这里的关键点是Agent Loop 不是把循环跑久一点就行而是要把状态、预算、停止条件和观察截断都显式管起来。这些就是 Harness 控制层在运行内核里的具体体现。4. 验证请求与 ReAct 步骤成功结果配置写完得验证它真的能跑。验证分两层先验证模型通道通不通再验证 Agent Loop 的 ReAct 步骤是否符合预期。先做通道验证。用最简单的请求确认 Base URL、Key、Model ID 三件套对齐。from openai import OpenAI client OpenAI( base_urlhttps://taotoken.net/api, api_keysk-your-key-here, ) resp client.chat.completions.create( modelclaude-3-5-haiku-20241022, messages[{role: user, content: 回复 OK 两个字母即可}], max_tokens16, ) print(resp.choices[0].message.content)如果这一步返回OK说明通道没问题。如果报 401说明 Key 不对如果报 model not found说明 Model ID 写错如果连接超时检查 Base URL 是否漏了/api路径。通道通了之后跑一个最小 ReAct 任务来验证 Loop。我用的测试任务是让 Agent 读取 workspace 下的一个文件统计行数然后报告结果。这个任务足够简单能在一两步内完成方便观察每一步的输入输出。def read_file(path: str) - str: import os full os.path.join(./workspace, path) with open(full, r, encodingutf-8) as f: return f.read() tools {read_file: read_file} loop AgentLoop(config, llm_client, tools) result loop.run(读取 workspace 下的 demo.txt告诉我它有多少行) print(json.dumps(result, ensure_asciiFalse, indent2))预期看到的步骤序列是这样的第一步模型推理出需要调用read_file参数pathdemo.txt。第二步工具返回文件内容。第三步模型根据内容计算出总行数返回 final 答案。整个过程step_count应该是 2 到 3。验证清单我列一下每一条都要实际确认检查项预期结果不通过的常见原因通道请求返回正常返回文本Key/Base URL/Model ID 不匹配工具调用解析decision.type 为 tool模型未按 Schema 输出工具执行返回文件内容路径错误或权限问题观察写回history 增加 tool 消息截断逻辑异常停止条件status 为 task_complete步数或预算设置过小token 统计token_used 有值usage 字段未读取跑通这个最小任务后可以逐步加复杂度换成需要多步的任务比如“找出 workspace 下所有 Python 文件里 import 了 requests 的文件列出文件名”。这个任务需要先搜索、再逐个读取、再判断能触发多轮 ReAct 循环也能验证 no-progress 检测是否生效。实测下来最容易出问题的环节是工具返回结果的格式。如果工具返回一大段未经整理的文本模型在下一步推理时容易被噪声带偏。所以observation_truncate_chars这个参数不要设太大4000 字符左右是个比较稳的起点。5. 本篇常见错误排查Agent Loop 跑不起来报错往往集中在几个地方。这一节按真实报错来对照排查。401 Unauthorized。这个最直接Key 不对或者没带上。检查api_key字段是否填了完整 Key有没有多余空格。如果用环境变量确认变量名和读取代码一致。还有一种情况是 Key 被撤销了去控制台重新生成一个。local proxy failed / connection refused。这类报错通常是 Base URL 写错。确认地址是https://taotoken.net/api注意结尾的/api不能少。有些客户端会自动拼接/v1/chat/completions如果你的客户端也这样Base URL 就填到/api这一层让它自己拼后面的路径。reading choices 报错 / choices 字段为空。这通常说明请求发出去了但返回结构和你解析的字段对不上。先打印完整响应体看看实际结构。有些兼容层返回的字段名可能略有差异按实际返回调整解析逻辑。如果返回里带 error 字段先看 error message。OAuth 相关报错。如果你用的是 Claude Code 或类似工具报 OAuth 错误说明认证方式配错了。这类工具需要的是 API Key 认证不是 OAuth 流程。检查配置文件里是否误开了 OAuth 模式改成 Key 认证。文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有对应说明。工具调用参数解析失败。模型返回的 tool_calls 里 arguments 是 JSON 字符串需要先json.loads再使用。如果模型输出的 JSON 不合法会抛解析异常。处理方式是在_parse_decision里加 try/except解析失败时把错误信息作为观察写回让模型重新生成。Loop 跑不完 / 一直不停止。检查max_steps和max_tokens_budget是否设置。如果设置了还是跑很久看 no-progress 检测是否生效。常见原因是观察结果每次都略有不同比如带时间戳导致哈希永远不一样no-progress 永远不触发。这种情况要把观察结果里的易变字段归一化后再哈希。CC Switch / Cline MCP / Codex auth.json 配置问题。如果你在这些工具里接入记住三件套要写全Base URL 填https://taotoken.net/apiKey 填生成的 KeyModel ID 填平台支持的模型标识。以 Codex 的auth.json为例结构大致是{ base_url: https://taotoken.net/api, api_key: sk-your-key-here, model: claude-3-5-haiku-20241022 }三个字段缺一不可。Cline 的 MCP 配置类似在设置里找到 API 配置区域把这三项填进去。CC Switch 切换配置时确认切换后的配置里这三项都完整。排查的核心思路是先确认通道通不通再确认 Loop 逻辑对不对最后确认工具执行有没有副作用问题。大部分报错在前两步就能定位。6. 从模型调用到 Agent 系统的下一步把上面的配置和代码跑通之后你手上就有了一个能循环推进任务的 Agent Loop。但要说清楚一点这只是 Harness 控制层的运行内核部分。完整的 Agent 系统还要往上叠状态管理、验证和可观测。状态管理这块最小实现是把history持久化到文件或数据库每次 Loop 启动时先读取上次的进度。这样即使进程崩溃重启后也能从断点继续而不是从头再来。Anthropic 的长任务案例里提到的claude-progress.txt和 Git 历史就是这个思路的工程化版本——让新会话读取外部事实而不是依赖压缩后的聊天摘要猜测。验证这块核心原则是完成由外部验收结果决定不由模型一句话决定。模型说“任务完成”不算数要跑测试、要检查产物、要有独立的验证步骤。可以在 Loop 里加一个verify阶段任务声明完成后自动触发检查检查不通过就把失败信息写回观察让模型继续修。可观测这块每次 Loop 的每一步都要记录step 编号、模型输入、模型输出、工具调用、工具返回、token 消耗、耗时。这些数据落到结构化日志里出问题时能回放整个过程。没有可观测性的 Agent 系统调试起来就是盲人摸象。如果你打算把这个 Loop 用到长期编码任务上可以考虑用 Coding Plan 来承载高频调用地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。对于需要频繁切换模型做验证的场景模型对话入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 可以手动对比不同模型在同一个 ReAct 步骤上的表现。最后说一个我踩过的坑一开始我把max_steps设得很大觉得步数多总能把任务跑完。结果发现模型在卡住的时候会反复调用同一个工具烧掉大量 token 却没有任何进展。后来把 no-progress 检测加上连续三步观察结果没有实质变化就强制停止并触发重新规划token 消耗直接降了一半。Agent Loop 的关键不是让它跑得久而是让它在该停的时候停得下来。这个停止条件的判断逻辑才是 Harness 控制层真正值钱的地方。
阅读完成 · 觉得有帮助?