Agent Harness 多轮进化实战:从单次 Run 工具循环到跨 Run 长期记忆的配置骨架 1. 为什么单次 Run 的工具循环跑通了跨 Run 还是“失忆”如果你已经写过一个最小可用的 Agent Harness大概率经历过这个阶段单次npm run dev里模型能连续调用list_files、read_file、edit_file一轮一轮把任务做完Trace 里 Turn 1、Turn 2、Turn 3 清清楚楚。可一旦退出进程下次再启动Agent 就像换了个人——你上一轮告诉它的项目约定、命名规范、正在改的文件全都不记得了。这不是模型的问题而是 Harness 的配置骨架只覆盖了“单次 Run 内的工具循环”没有把“跨 Run 的长期记忆”接上。单次 Run 内的多轮Intra-run turns解决的是“一个任务需要多个工具配合”的问题跨 Run 的多轮Session解决的是“多个任务之间需要上下文连贯”的问题。两者维度不同但配置上必须衔接好否则会出现工具调用格式错乱、上下文爆炸、记忆污染等一连串问题。这篇就围绕 Agent Harness 的配置骨架展开先给出可复制的config.toml与settings.json再说明如何通过统一的 Key/API 通道接入 TaoToken最后给出验证跨 Run 记忆是否真正生效的具体动作以及我在 Turn 边界、Session 存储、消息裁剪上踩过的坑。适合已经跑通单次 Run、准备把 Agent 变成“能记住事”的开发者。2. 前置准备统一 Key/API 通道接入 TaoToken在动配置之前先把模型通道固定下来。Agent Harness 的长期记忆依赖稳定的模型调用如果每次 Run 都换 Key、换 Base URLSession 恢复时很容易因为鉴权或模型名不一致而失败。我的做法是统一走 TaoToken 的 API 通道Key 只维护一份。TaoToken 的 API 地址是https://taotoken.net/api兼容常见的 OpenAI 风格调用方式。你需要在控制台创建一个 API Key然后把它写进环境变量而不是硬编码进config.toml。这样 Session 文件里不会残留密钥跨机器迁移也安全。创建 Key 的入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite拿到 Key 之后写入 shell 环境export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你用的是 Claude Code 这类编码 AgentTaoToken 也提供了对应的接入文档配置方式略有差异https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意不要把 Key 直接写进settings.json提交到 Git。Session 目录和配置文件建议加进.gitignore只保留.env.example。通道固定后Harness 里所有模型调用都指向同一个 Base URLSession 恢复时不会因为端点漂移导致tool_call与tool_result配对失败。3. 可复制配置骨架config.toml 与 settings.json配置分成两层config.toml管运行时行为Turn 上限、Session 策略、消息裁剪settings.json管模型与工具声明。两者职责分离改行为不用动模型声明。3.1 config.tomlRun 与 Session 的衔接参数# config.toml [run] max_turns 8 # 单次 Run 内最多几轮工具循环 turn_timeout_ms 60000 # 单轮模型调用超时 early_stop_on_no_tool true # 模型没调工具且 finish_reasonstop 时视为 final_answer [session] enabled true store_dir ./sessions # Session 文件目录 max_messages 40 # 跨 Run 保留的最大消息条数 persist_tool_messages false # 关键不持久化 tool_call / tool_result last_pointer _last.json # 指向最近一次 Session 的指针文件 [context] system_prompt_file ./prompts/system.md include_trace_snapshot true # 每轮结束写 messages 快照便于复盘 [model] provider taotoken base_url_env TAOTOKEN_BASE_URL api_key_env TAOTOKEN_API_KEY model deepseek-chat temperature 0.3这里最关键的两个参数是persist_tool_messages false和max_messages 40。前者决定跨 Run 只存文本对话后者决定上下文不会无限膨胀。原因在排障章节展开。3.2 settings.json模型与工具声明{ model: { name: deepseek-chat, context_window: 65536, max_output_tokens: 4096 }, tools: [ { name: list_files, description: 列出指定目录下的文件, parameters: { type: object, properties: { path: { type: string, default: . } } } }, { name: read_file, description: 读取文件内容支持行范围, parameters: { type: object, properties: { path: { type: string }, start_line: { type: integer }, end_line: { type: integer } }, required: [path] } }, { name: edit_file, description: 局部替换文件内容old_string 必须完全匹配, parameters: { type: object, properties: { path: { type: string }, old_string: { type: string }, new_string: { type: string }, replace_all: { type: boolean, default: false } }, required: [path, old_string, new_string] } } ], session: { restore_strategy: text_only, trim_strategy: keep_recent, trim_keep: 40 } }restore_strategy: text_only与config.toml里的persist_tool_messages false是同一件事的两处声明Harness 启动时以settings.json为准config.toml作为默认值兜底。这样设计的好处是你可以针对不同项目放不同的settings.json而config.toml保持通用。3.3 Session 文件结构Session 存储目录长这样sessions/ _last.json # { sessionId: s-20260701-abc } s-20260701-abc.json # 该 Session 的文本对话 s-20260630-xyz.json单个 Session 文件内容只保留user和assistant文本消息{ sessionId: s-20260701-abc, createdAt: 2026-07-01T10:00:00Z, updatedAt: 2026-07-01T10:12:00Z, messages: [ { role: user, content: 记住本项目用 pnpm不用 npm }, { role: assistant, content: 好的已记住使用 pnpm。 } ] }工具调用过程不落盘这是跨 Run 恢复时 API 格式不乱的前提。4. 验证跨 Run 记忆是否生效三个具体动作配置写完不代表记忆就生效了。下面三个动作可以逐层验证Turn 循环是否正常、Session 是否落盘、跨 Run 是否真的恢复。4.1 动作一单次 Run 内跑出多轮 Turn先确认单次 Run 的工具循环没问题。执行npm run dev -- 列出根目录文件然后读取 README.md 前 20 行预期 Trace 输出类似turn_started (turn1, messages2) turn_model_response (tools1) tool_call list_files - ok (75 files, 13ms) turn_context_snapshot (messages4) turn_done (reason: tool_observations) turn_started (turn2, messages4) turn_model_response (tools1) tool_call read_file - ok (8180 bytes, 1ms) turn_context_snapshot (messages6) turn_done (reason: tool_observations) turn_started (turn3, messages6) turn_model_response (tools0, assistantPreview) turn_done (reason: final_answer, finishedtrue)如果 Turn 2 之后直接final_answer说明max_turns或early_stop_on_no_tool配置有问题先回到第 3 节检查。4.2 动作二确认 Session 落盘第一次 Run 用--new显式创建 Sessionnpm run dev -- --new 记住我在学 Agent Harness项目用 pnpmRun 结束后检查cat sessions/_last.json cat sessions/s-*.json你应该看到_last.json里的sessionId与目录下某个文件对应且该文件messages数组里有两条文本消息没有tool_call字段。如果messages为空说明session_saved事件没触发检查[session] enabled是否为true。4.3 动作三跨 Run 恢复并追问关键验证新开一个进程用--continue恢复npm run dev -- --continue 我刚才说项目用什么包管理器预期模型回答“pnpm”。Trace 里应出现session_loaded (messages2) turn_started (turn1, messages4) # 2 条历史 1 条新 user system turn_done (reason: final_answer)如果模型答“不知道”按顺序排查_last.json指针是否指向正确文件、restore_strategy是否为text_only、恢复时是否把历史消息拼在了 system prompt 之后。这三步过了跨 Run 记忆就算真正生效。5. 本篇常见错排查5.1 报错tool_result 必须紧跟 tool_call现象--continue恢复后第一次模型调用直接 400提示tool_call_id找不到对应消息。原因你把tool_call和tool_result也持久化了。跨 Run 恢复时历史里的tool_result找不到同一 Run 内的tool_callAPI 格式校验失败。解法确认persist_tool_messages false并清理已有 Session 文件里残留的工具消息。工具过程是“过程性”信息跨 Run 需要的是“结论性”信息这个边界要守住。5.2 报错上下文超长导致 400现象连续--continue十几次后模型调用报context_length_exceeded。原因max_messages没生效或者裁剪策略是keep_all。解法把trim_strategy设为keep_recenttrim_keep设为 40。40 条消息按中文平均 500 token 估算约 20K token给 64K 窗口留足工具调用和 system prompt 的余量。这个数字可以按你的模型窗口调整但不要不设上限。5.3 现象模型“撒谎”说完成了现象turn_model_response里模型说“我已经完成了”但tools0实际任务没做完。原因finish_reason是stop但模型只是暂停思考并非真正给出最终答案。解法在turn_done判断里加规则——本轮没调工具且finish_reasonstop时按final_answer处理但在 Trace 里标记early_stop。这样既不把交互推给用户又能在复盘时定位模型行为异常。config.toml里的early_stop_on_no_tool true就是干这个的。5.4 现象--continue和--new语义混淆现象用户想“继续上次话题”结果--new开了新 Session历史丢了。解法把三个开关的语义写进 CLI 帮助并在 Trace 里输出session_loaded/session_saved/session_cleared事件。--continue加载上次 Session不存在则报错--new创建新 Session 并更新_last.json指针--forget删除指针和对应文件。语义清晰后误操作会大幅减少。5.5 现象Session 文件越来越大现象sessions/目录几个月后占了几百 MB。原因只增不删历史 Session 文件一直保留。解法加一个清理命令按时间或数量保留最近 N 个 Session。当前阶段可以手动清理但要在文档里写明“历史 Session 文件不会自动删除”。这是故意留的简化先跑通最小可用版本再根据真实使用情况决定是否加自动清理。6. 把配置跑起来从单次 Run 到跨 Run 记忆的完整链路回到最初的问题单次 Run 的工具循环和跨 Run 的长期记忆配置上怎么衔接答案是把它们拆成两个正交的维度各自管好自己的状态只在 Session 恢复时做一次“文本层”的拼接。单次 Run 内max_turns控制工具循环上限turn_done的reason区分final_answer和tool_observationsTrace 事件让每一步可观测。跨 Run 时SessionStore只持久化user和assistant文本_last.json指针负责定位max_messages负责裁剪。两者通过restore_strategy: text_only衔接工具过程不跨 Run避免 API 格式错乱。如果你准备把这套骨架接到自己的项目里建议先跑通第 4 节的三个验证动作再逐步加工具。模型通道统一走 TaoToken 的 APIKey 从环境变量读Session 目录加进.gitignore。需要长期编码或 Agent 场景的话可以了解 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite想先验证模型对话行为可以从模型对话入口试起https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite我实测下来Turn 事件和 Session 事件分开记录后Debug 从“一团黑盒”变成了“可逐步复盘的流水线”。下一步我打算把max_messages做成按模型窗口动态计算再给 Session 加一个简单的压缩策略——但那是另一个坑了先把当前这套配置跑稳。