
搞LLM应用这一年多我最大的感受就是让模型写一段漂亮回复根本不算本事真正磨人的是把一个智能体Agent放到真实场景里反复跑让它不崩、不乱、不烧钱。这个过程中我折腾最多的就是标题里的Agent Harness这个东西——中文语境下可以叫智能体控制器——尤其是上下文管理和编排这两块几乎每一个上线项目都踩过一遍。先说清楚这篇文章是什么。它不是理论综述也不是某个框架的广告而是我自己实践笔记的完整整理从最朴素的“一个while循环套prompt”开始逐步讲清楚Harness到底解决了什么问题、上下文怎么管才不会爆、编排怎么做才不会乱、容错怎么写才不会崩最后给出一套可以直接抄的笔记类Agent Harness代码骨架。适合正在做Agent落地、被上下文炸掉、或者Agent跑着跑着就自行其是的工程师和产品团队参考。1. Agent Harness 到底是什么先搞懂它管哪些事1.1 从裸写循环说起没有 Harness 的日子最早我做的所谓Agent其实就是一个while循环加一个精心设计的prompt。模型输出解析失败就重试上下文一股脑全塞进去工具调用的结果也不校验跑demo的时候一切正常一上真实数据就原形毕露生成长文时上下文直接爆掉、模型不懂得收敛、同一个工具反复调用、偶尔还会编造根本不存在的函数名。这些问题的根源不是模型能力而是外层缺少一套“让Agent安全运行”的控制装置。你想想一辆车跑得快不快看发动机但方向盘、刹车、仪表盘这些都不在发动机里。对LLM来说Agent Harness就是那套方向盘和仪表盘。Harness这个英文词原意是把马套上车的那套挽具。放在智能体工程里它就是介于LLM和具体业务之间的一层控制框架负责驱动Agent循环、管理上下文、调度工具、处理错误和持久化状态。简单说模型是发动机Harness是驾驶室。1.2 Harness 和 Agent 的区别别再混为一谈这是我被问得最多的问题也是很多项目设计混乱的根源。Agent指的是业务流程中能做决策、能执行动作的那个执行体它关心的是目标、策略和工具选择而Harness是承载Agent的运行环境它关心的是循环怎么转、状态放哪、出错怎么办。我常用的一个类比Agent是演员Harness是剧组。演员负责演戏剧组负责剧本分发、场记、灯光、救场。你直接调LLM接口拿到的那段JSON只是演员台词而你自己写的那个循环、状态管理、工具注册表、召回逻辑、限流熔断全是剧组的工作。在代码层面很多开源框架把这两层揉在同一个类里比如LangChain的AgentExecutor既管决策也管执行。但概念上必须分开否则你很难回答一个问题如果Agent决策错了是模型的问题还是Harness的问题分清楚之后排查效率会高非常多。1.3 Harness 的核心职责清单根据我自己的项目经验一个合格的Harness至少要覆盖以下八件事驱动“思考-行动-观察”循环并控制循环何时终止维护Agent状态包括当前任务、历史轨迹、中间变量将长期记忆和检索结果按需注入上下文而不是无脑拼接维护工具注册表负责参数校验和调用规范解析模型输出并对不合规输出做修复和重试处理异常包括超时、限流、工具报错、上下文溢出记录完整运行轨迹方便离线回放和问题定位统计token消耗和成本防止预算失控这八条看着简单每一条展开都是一堆细节。接下来我挑最折磨人的两块——上下文管理和编排——重点讲。2. 上下文管理Agent 的工作台与记忆系统2.1 先算清上下文这本账你的窗口真的够用吗很多团队一上来就选128K窗口的模型觉得上下文肯定够用其实根本不是这么回事。我以笔记Agent为例给你算一笔账系统提示词写清楚角色、规则、输出格式大约2000 token用户任务描述比如“整理五月份第三周的会议纪要”约300 token工具定义每个工具的描述和参数Schema大约300 token注册8个工具就是2400 token对话历史每轮Agent思考加工具调用输入输出合计大约1500 token10轮就是15000 token检索出来的笔记内容按Top 3召回每篇500 token又是1500 token算下来固定开销就5000 token左右这还没开始干活。真正跑起来每多一轮工具调用输入输出都会增长。128K的窗口看着很大但模型对长上下文的注意力会衰减中间位置的信息最容易被忽略学术界管这个叫lost in the middle。所以上下文管理的核心不是“塞得下”而是“要塞得准”。我的实操经验是把窗口的70%作为上下文预算剩下的30%是给模型回复和意外情况留的缓冲。具体公式是可用历史长度 上下文窗口 × 0.7 - 固定开销系统提示词 工具定义 任务描述这个预算再分给“工作记忆”和“检索注入”两部分谁重要谁先拿。2.2 三层上下文架构核心记忆、工作记忆、外部存储我把Agent的上下文设计成三层这是目前最稳定、改造成本最低的方案。第一层叫核心记忆就是系统提示词加用户当前任务。系统提示词里只放不会变的东西角色定位、输出格式、安全边界、工具使用总原则。用户任务描述单独放因为每次任务不同。这两块是上下文中优先级最高的部分永远保留。第二层叫工作记忆就是当前任务最相关的对话轨迹和中间结果。我会保留最近5轮完整的思考过程更早的轮次通过摘要压缩。为什么只留5轮因为Agent的中间推理有很多是重复的比如反复比较同一批候选笔记保留全量纯属浪费token。第三层叫外部存储就是不在上下文里、但随时可以检索回来的东西。对我们笔记场景来说就是全部Markdown笔记的向量索引加摘要库。需要用的时候通过检索把最相关的几篇注入到工作记忆用完就丢。这三层各司其职之后我几乎没有再遇到过上下文爆掉的问题。2.3 上下文压缩与裁剪三种手段的配合打法压缩不是简单截断而是分场景用不同手段。第一是近因滑动窗口。只保留最近N轮完整消息之前的全部移出上下文。这个方案简单粗暴适合处理那些历史中确实没有重要信息的场景。缺点也很明显如果第2轮出现过关键决策第10轮要引用时已经没了。第二是摘要压缩。我让LLM每两轮就把前面的对话总结成一段结构化的要点摘要替换到系统提示词里。注意摘要不是自由发挥我固定了格式必须包含已完成事项、待办事项、关键结论、当前不确定项。这样一个1000 token的长对话可以被压到150 token左右信息损失可控。代价是每次压缩会额外消耗一次模型调用所以要设好压缩触发条件比如窗口占用超过预算60%时才压缩。第三是结构化检索。笔记内容永远不直接全量进上下文而是先进向量库查询时用语义召回TopK动态拼装成临时上下文块。这块如果要展开其实就是RAG那一套分块、embedding、向量检索、重排序。我用的分块策略是按标题切分Markdown的二级标题和三级标题作为天然边界每个块控制在400到600 token之间既保证语义完整又方便精准召回。三种手段配合的节奏是平时靠滑动窗口控制长度窗口快满时触发摘要压缩涉及具体笔记内容时走结构化检索。2.4 上下文一致性的坑旧数据害死人上下文管理里最隐蔽的问题不是长度是一致性。我踩过一次大坑笔记Agent在第二轮引用了某篇笔记的旧版本原因是第一轮检索的结果被缓存了而那篇笔记在第二轮开始前已经被用户修改过。结果Agent基于过期内容做了一堆错误判断。现在我的所有上下文块都带版本标记和时效信息。工具返回的数据会标注“获取时间”检索结果会标注“笔记最后修改时间”系统提示词里明确写了一条规则如果发现上下文中存在互相矛盾的信息以标注时间更新的为准并且要把数据冲突作为一个新事实记录到工作记忆而不是闷头往下走。另外同一篇笔记在同一轮里只允许出现一次。如果滑动窗口和检索结果都包含同一篇笔记的不同版本Harness会做去重合并选版本号最高的那个。这个小功能看着不起眼避免了大量认知混乱。3. 编排实践让 Agent 按节奏干活而不是即兴发挥3.1 工作流编排与自主编排两条路线的取舍编排这个词被用烂了但实际只有两条路线。工作流编排就是预先画好流程图Agent只能沿着固定节点走。比如笔记整理流程定为“检索→理解→更新→校验”四步每步之间是硬衔接模型不能跳步。优点是行为可预测、好调试缺点是灵活性差遇到计划外场景容易卡死。自主编排就是经典的Agentic Loop模型每轮自己决定下一步干什么。优点是能处理开放任务缺点是失控风险高你不知道它下一步会调哪个工具也不知道什么时候能停下来。我的选择是混合编排固定骨架加局部自主。以笔记类任务为例整体流程固定为“定位笔记→提取信息→执行修改→自检确认”但每一步内部模型可以自己决定用哪个工具、查几个来源、先看摘要还是先看全文。这样既保住了关键路径的确定性又没牺牲局部灵活性。3.2 ReAct 模式思考与行动的循环拆解ReAct是目前Agent实现的主流范式核心是Reasoning加Acting交替进行。每一轮循环包括三段内容Thought思考模型分析当前状态说明为什么这么做Action行动调用某个工具带着参数Observation观察工具返回的结果这三段周而复始直到模型输出Final Answer。我举个实际轨迹你就明白这个模式长什么样Thought: 用户想整理本周的会议纪要我需要先找到本周相关的笔记文件。 Action: search_notes(query本周 会议纪要 周会, top_k3) Observation: 找到三篇笔记分别是周一产品评审、周三技术方案会、周五项目同步会。 Thought: 三篇笔记都存在我需要逐篇提取待办事项先从第一篇开始。 Action: read_note(note_idnote_1001, section待办行动项) Observation: 提取到5个待办事项其中两个已完成三个进行中。 ...这个模式的好处是每一步都可解释、可回放Harness可以清楚地知道Agent在哪一步出错。坏处是轮次多了token消耗大所以一定要和第二节的压缩策略配合。3.3 我的 Harness 编排层实现一个状态机骨架我代码里的编排层不是一个while循环而是一个简单的状态机。状态包括IDLE等待任务THINKING调用LLM生成Thought和ActionACTING执行工具调用OBSERVING处理工具返回结果FINISHED输出最终答案状态转移规则是IDLE收到任务后进入THINKINGTHINKING产出Action后进入ACTINGACTING执行完进入OBSERVINGOBSERVING把结果拼回上下文再回到THINKINGTHINKING产出Final Answer时进入FINISHED。任何状态超时或异常都进入ERROR状态由容错模块接管。这个状态机的价值在于你知道Agent当前在哪一步而不是只有一个笼统的“运行中”。出问题时能精确说是执行工具崩了还是模型输出格式坏了还是上下文超限了。3.4 任务拆分与优先级避免 Agent 抓不住重点开放式任务经常一上来就是个大目标比如“整理这个月的所有笔记”。如果让Agent直接干它很容易迷失一会儿改这篇一会儿翻那篇最后什么都没完成。我现在的做法是在Harness层先做任务分解。任务分解的策略是先让LLM把大目标拆成可独立验证的子任务每个子任务带依赖关系和预估成本。比如“整理这个月的所有笔记”会被拆成子任务A列出本月全部笔记清单预估20篇子任务B按主题聚类识别出会议记录、技术笔记、灵感碎片三类子任务C逐类提取关键待办和执行状态子任务D合并去重生成月度总结笔记执行顺序由依赖关系决定B依赖AC依赖BD依赖C。优先级则参考三个因素用户是否在任务描述里强调了某个方向、子任务的依赖深度、token成本。Harness会维护一个任务队列每完成一个子任务就更新全局状态让Agent时刻清楚“我已经做到哪一步、还剩什么”。4. 容错控制与自愈设计构建不崩的Agent系统4.1 Agent 的失败模式清单先知道会怎么死容错设计的第一步是枚举失败模式。我整理过一张清单几乎涵盖了所有踩过的坑输出格式错乱模型没有按约定的JSON格式输出多写了注释、少了括号工具幻觉调用了不存在的工具或者工具存在但参数编造无限循环Agent反复执行同一个动作比如反复搜索同一个关键词上下文污染工具返回了大量无用信息把关键信息冲淡幻觉知识Agent在没有依据的情况下编造笔记内容外部依赖故障向量库超时、embedding接口限流、文件系统权限错误这些失败模式里只有少数能靠换更强的模型解决大多数必须在Harness层用工程手段拦截。4.2 三类容错策略前置校验、运行时控制、后置纠偏我把容错手段分成三层按从上游到下游的顺序排列。前置校验是在工具调用执行之前做的。每个工具都定义了参数Schema模型输出Action后Harness用轻量校验器检查参数类型和枚举值范围不合格的直接拦截不给工具执行机会。同时维护一个工具白名单模型只能调用白名单内部署好的工具从根上消灭工具幻觉。运行时控制是执行过程中做的。核心参数有三个最大迭代次数我通常设10到15超过就强制终止单次工具超时我设30秒到60秒连续相同动作检测如果模型连续3轮输出同样的Action和参数视为死循环直接打断并提示模型换个策略。后置纠偏是结果出来之后做的。如果模型输出的JSON解析失败但内容语义完整我会用一次“结构化修复”调用把原始输出交给模型重写为合法JSON最多重试2次。如果最终答案出现事实性矛盾则触发“无依据自动纠偏”让Agent回退到最近一个可信状态提示用户补充信息。4.3 让 Agent 学会说“我不知道”验证器与护栏这是很多Agent系统缺失的一环。大多数内置Agent倾向于编造答案因为模型的训练目标就是补全文本它没有“不知道”这个选项。Harness必须强行加一道护栏所有Action必须挂载验证函数验证失败时自动切换到“需要更多信息”分支触发追问或切换检索策略。比如在笔记场景里Agent要输出一条行动项但这条行动项无法溯源到任何一篇笔记内容验证器就会拒绝并要求Agent返回指定搜索的观察结果作为证据。这种“无证据不输出”的机制让系统在面对模糊查询时从“硬编答案”变成了“承认信息不足并主动补全”。4.4 可观测性Agents 也需要事后回放没有轨迹记录的Agent系统就是黑盒出了问题只能靠猜。我现在的做法是给每一次Agent运行生成一份结构化trace存到SQLite里。trace包含全局运行ID和父级ID每轮的Thought、Action、Observation摘要调用的工具名、输入参数摘要、输出摘要每一步的耗时和token消耗错误信息和重试次数这份trace我称之为Agent的“飞行记录仪”。线上问题出现后我第一件事不是看代码而是回放trace看模型在哪一轮开始跑偏。有几次排查效率从几小时缩短到十几分钟靠的就是这个。除了单体Agent的trace多Agent协作场景下还要给编排层加分布式追踪的思路用trace_id串联所有子任务形成一棵完整的执行树。5. 完整实操案例从零搭一个笔记 Agent Harness5.1 场景与需求定义我从零搭一个“漫游笔记Agent”它的工作是在一堆Markdown笔记之间检索、提炼、整理、归档。用户输入一句自然语言指令Agent负责找到相关笔记、提取关键信息、按规则生成新笔记或修改旧笔记最后输出一份操作摘要。这个场景非常适合验证Harness设计因为它涉及检索、读取、写入、总结、校验五类工具而且每个操作之间都有依赖上下文天然会膨胀。硬件和依赖方面我用的是Python 3.11加一个轻量级异步框架没有依赖LangChain那种重框架因为我想把Harness的每一行逻辑都握在自己手里。LLM用支持工具调用的通用模型即可向量检索我用本地的轻量库整个系统可以不依赖外部服务运行。5.2 核心代码骨架一个可运行的 Harness下面是我实际项目里简化后的核心骨架保留了Harness的关键逻辑你可以直接抄走改改。import json import time from dataclasses import dataclass, field from typing import Callable, Dict, Optional dataclass class Tool: name: str description: str parameters_schema: dict handler: Callable validate: Callable lambda **kwargs: None dataclass class AgentContext: system_prompt: str messages: list field(default_factorylist) memory_budget_tokens: int 8000 tool_call_versions: dict field(default_factorydict) class AgentHarness: def __init__(self, llm, tools: Dict[str, Tool], config: Optional[dict] None): self.llm llm self.tools tools self.config config or { max_iterations: 12, max_retries: 2, timeout_seconds: 45, token_ratio: 0.7, } self.trace [] async def run(self, task: str, notes_index) - dict: ctx AgentContext(system_promptself._build_system_prompt(notes_index)) ctx.messages [ {role: user, content: task} ] for i in range(self.config[max_iterations]): # 1. 控制上下文长度超出预算则压缩 self._enforce_budget(ctx) # 2. 调用模型生成下一轮动作 response self.llm.invoke(ctx.messages) action self._parse_action(response) # 3. 如果模型输出最终答案直接返回 if action[type] final: return self._finalize(ctx, action[answer]) # 4. 校验工具参数失败则重试 ok, validated_args self._validate_action(action, ctx) if not ok: ctx.messages.append(self._retry_prompt(action, 参数校验失败)) continue # 5. 执行工具调用并记录观察结果 observation await self._execute_tool(validated_args, ctx) ctx.messages.append({ role: tool, tool_call_id: action[call_id], content: observation, }) self._record_trace(i, action, observation) return {status: max_iterations_exceeded, partial_result: self._extract_partial(ctx)} def _build_system_prompt(self, notes_index) - str: # 核心记忆区角色、规则、工具使用约束、索引说明 prompt f你是一个知识库笔记助手。当前知识库索引如下 {notes_index.describe()} 规则 1. 所有事实必须来自工具观察结果不得编造。 2. 使用工具的步骤中每次只调用一个工具。 3. 引用笔记时必须标注笔记ID。 4. 最终答案需包含操作了哪些笔记、提取了哪些关键信息、遗留哪些疑点。 return prompt def _parse_action(self, response): # 尝试解析JSON失败就返回特殊错误让上层修复 text response.content.strip() try: return json.loads(text) except json.JSONDecodeError: return {type: parse_error, raw: text} def _validate_action(self, action, ctx): tool_obj self.tools.get(action.get(tool_name)) if not tool_obj: return False, None # 这里可以用jsonschema做严格校验省略细节 try: validated tool_obj.validate(**action.get(arguments, {})) return True, validated except Exception: return False, None async def _execute_tool(self, validated_args, ctx): tool_obj self.tools[validated_args[tool_name]] try: result await tool_obj.handler(**validated_args[arguments]) return json.dumps(result, ensure_asciiFalse, defaultstr) except Exception as e: return fTOOL_ERROR: {str(e)} def _record_trace(self, i, action, observation): self.trace.append({ step: i, time: time.time(), action: action, observation_summary: observation[:200], })这段代码最关键的设计是_enforce_budget和_validate_action。前者做上下文压缩后者做工具前置拦截。整个运行循环的终点有两个模型主动输出Final Answer或者迭代次数耗尽强制退出。第二种情况下返回“partial result”后续容错模块可以接管。5.3 实测效果与参数调优别信默认参数我拿一份20篇混乱的Markdown笔记做了压测原始笔记总字数大概3万如果全部塞进上下文一次调用就会超过8K预算。使用上面的Harness后每轮平均上下文稳定在3000 token左右原因是滑动窗口和摘要压缩把历史控制住了检索只注入Top 3。实测下来几个关键参数的经验值参数我的推荐值说明max_iterations12超过12轮还未完成的任务大概率需要重新规划top_k检索3一次检索返回3篇太多会稀释注意力摘要压缩阈值预算60%低于这个值不压缩频繁压缩反而更贵解析失败重试次数2连续3次失败说明outpout结构不适合temperature0.2工具调用场景温度越低越稳定调参过程里最意外的是temperature。我早期用0.7Agent经常在工具参数上发挥创意降到0.2之后工具幻觉直接下降了大约七成。如果你的Agent频繁编造参数或工具名先降温度再考虑换模型。6. 常见问题与排查技巧实录6.1 上下文越管越乱Agent 前后矛盾这是最常见的翻车现场。一般来说问题不在滑动窗口的轮数而在压缩时机。太早压缩会把关键决策丢掉太晚压缩等于没压。我调试时的第一件事是看trace里的摘要块。如果摘要丢失了某个关键待办事项就调整压缩触发逻辑把“待办事项完整性检查”加到压缩提示词里让模型在生成摘要前先对照原历史做一次查漏。另一个隐藏问题有的实现会把压缩后的摘要和原始历史都留在上下文里导致一个信息两个版本Agent引用时随机选一个。我的经验是压缩动作一旦完成原始历史必须从上下文清出去只留摘要引用。6.2 无限循环Agent 反复做同一个动作出现这个现象多半是Observation没有给模型提供有效的新信息。比如模型搜索“周会纪要”返回为空它不甘心又搜了一遍完全一样的关键词。这类死循环靠max_iterations兜底能防住但更聪明的做法是修改Action去重逻辑。我在Action执行前会检查一个动作指纹工具名加参数哈希。如果最近三跳内出现过完全一致的指纹就不执行直接返回给模型一条提示“该动作与前序动作重复且未产生新结果请更换关键词或切换检索范围”。6.3 频繁触发重试但依然解析失败一次两次解析失败是运气连续失败就是系统问题了。常见的责任人是输出格式约束写得不够清楚。我发现对模型最有效的格式约束是给一个完整的JSON示例而不是只给字段定义。如果你已经给了示例模型还是乱输出检查一下系统提示词里是否同时出现了多套格式要求。我曾经把“要求输出JSON”和“要求输出Markdown列表”两句话都留在提示词里模型一会儿输出JSON一会儿输出Markdown换了更复杂的模板才稳定下来。6.4 检索结果不相关Agent 拿着噪声硬干活检索质量会影响Agent的决策但很多rank问题不是embedding模型的问题而是分块策略的问题。我踩过最蠢的一个坑把整篇5000字的笔记当成一个块去embedding结果语义向量被严重稀释查询“待办事项”时根本召不到藏在笔记中段的行动项。现在我的分块标准是按二级标题切分每块400到600 token块之间允许20%重叠保证跨标题的上下文不断裂。改为这个策略后检索命中率提升非常明显。6.5 成本失控Agent 不贵贵在反复试错最后提醒一下成本问题。一次Agent运行动辄5到10轮调用每轮输入里都带着浓缩后的历史累计token远超你写一个prompt的成本。我监控成本的核心指标不是单次调用价格而是“完成一个标准任务的平均成本”。压测数据给我一个参考整理一份周报笔记当Agent稳定运行时大概需要6轮调用总输入输出合计约4.2万token如果Agent出现频繁重试这个数字会翻到8万以上。所以成本优化的重点不是换更便宜的模型而是减少无效重试和无效检索。我现在给Harness加了一条成本告警规则单任务token数超过同类任务历史均值的2倍时自动挂起任务并通知人工审查。这条规则已经拦下了好几次因上下文污染导致的成本失控。我个人在实际操作中最大的体会是Agent Harness的设计没有银弹它是一门用工程手段给模型“画跑道”的手艺。模型每多一分自由Harness就要多一分约束模型每多一分能力Harness就要多一分校验。上下文管理解决的是“让模型看得清”编排解决的是“让模型走得稳”容错解决的是“让模型摔不坏”三者缺一不可。最后再分享一个和开头呼应的小技巧如果你还在用裸while循环套prompt的方式做Agent别急着上重框架先把这篇文章里的五件事补上——迭代上限、预算压缩、参数校验、轨迹记录、成本告警。这五个点花不了多少代码量但能把一个“能跑”的demo变成一个“敢上线”的系统。