ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent工程四层拆解:Prompt、Context、Harness与Loop实战指南

Agent工程四层拆解:Prompt、Context、Harness与Loop实战指南 我们不聊概念聊聊工程。标题里那四个词——Prompt、Context、Harness、Loop——不是四个并列的技术名词而是Agent系统在运行时真实发生的四种过程。很多团队做AI Agent代码写了不少最后卡在“为什么我的Agent跑几步就乱”“为什么上下文一长就崩”“为什么框架一换全得重写”根子往往不是模型选得不好而是这四块工程没拆开看。这篇万字长文我把它压缩成一篇尽量不啰嗦的实战拆解。核心回答三个问题Agent的工程体系到底由什么组成每一层解决什么真实问题以及你在实操中会踩到哪些具体报错与坑。适合正在搭Agent、做AI工作流、或者想从“调 Prompt”升级到“做 Agent 系统”的开发者。先说一个很关键的判断Agent 和 LLM 是两码事。LLM 只是一个文本推理引擎你给他一段输入他给你一段输出。Agent 是基于这个引擎构建的一个自治系统它有目标、有工具、有记忆、有循环能在一个任务上反复迭代直到完成。你会问“DeepSeek 属于哪个”DeepSeek 是 LLM 模型它既不是 Agent 也不是 Harness。Agent 往往是“模型 工程逻辑”的组合体。下面我把这个组合体拆开讲。1. 内容整体设计与思路拆解1.1 为什么我们必须把 Agent 拆成四层来看先摆一个铁律你不拆层就没法排障。很多人的 Agent 代码里prompt 写一大坨工具调用散落在各处会话历史拼接在业务代码里while 循环的退出条件靠“感觉”。结果问题一出现你根本不知道是哪一层出了问题是提示词没写清是上下文被截断是工具调用挂在框架层还是循环条件永远不满足你不知道。把系统分成 Prompt、Context、Harness、Loop 四层本质上是四层独立的“关注点分离”。每一层解决一类独立的问题Prompt 工程解决“模型如何理解任务”的问题管的是模型的输出质量。Context 工程解决“模型能看到的材料边界”的问题管的是输入的有效性和裁剪策略。Harness 工程解决“模型如何调用外部能力”的问题管的是工具的注册、执行、返回值回传。Loop 工程解决“系统如何推进任务直到完成”的问题管的是迭代终止条件、状态转移、人工介入点。注意这四层之间有依赖关系但不是线性的。Prompt 决定了 Loop 中每一步“做什么”Context 决定了模型“依据什么做”Harness 决定了模型“能做什么”Loop 决定了“做多久”和“什么时候停”。四个问题各自独立又组合成一个整体。1.2 常见误区把 Agent 工程当成“长 Prompt”我在社区看过太多“Agent 教程”本质上就是给你一段几百行的 system prompt告诉你“把这段塞进去你的 LLM 就是 Agent 了”。这是最大的误导。一段再华丽的 prompt 也不可能让模型自己去调用你本地的 Python 环境、去访问数据库、去重试失败的任务——因为它没有那个“手”。只靠 Prompt 撑起来的 Agent跑几个回合就会暴露两个致命问题第一没有工具回路。模型说“我要查天气”你如果只靠 prompt 让它“想象”查天气的结果它就会一本正经地编一个。这不是模型笨是你没有给它一个真实的工具。第二没有状态更新机制。Agent 每一步执行完系统需要把观察结果写回上下文再把新状态交给模型让它决定下一步。这个过程如果没有独立的 Context 管理层全用手写字符串拼接字符串三个回合后格式就乱了。所以我反复跟团队说Agent 的复杂度不是模型给的是工程结构给的。模型只是个计算体真正决定系统智能上限的是 Prompt 的质量、Context 的管理、Harness 的丰富度和 Loop 的稳健性。2. Prompt 工程Agent 的驾驶舱2.1 从“一次性指令”到“运行时指令”传统 Prompt Engineering 关注的是“怎么让模型回答好一个问题”到了 Agent 时代关注点变了Prompt 是运行时动态生成的而不是你写在代码里的一个常量。什么意思你的 Agent 在循环里跑每一步看到的系统提示词可能是一样的但用户指令、工具返回结果、历史执行记录这些是动态拼进去的。写 Prompt 的逻辑要从“写一段静态文本”转成“写一套模板 规则”。这里面有个核心原则静态指令管行为动态指令管事实。静态指令告诉模型“你是我的 AI 助手调用工具时要输出 JSON”动态指令携带本轮任务的具体内容“用户刚刚问的订单号是 A12345查询工具返回了以下三条结果”。我见过很多失败的案例是把动态数据全部堆进 system prompt导致 prompt 越来越长最终触发这类错误prompt is too long api error: 400 this models maximum context length is 1048576 tokens. However, ...你看一旦 prompt 过长被模型输入窗口拒收整个 Agent 循环直接中断。这就是为什么我们说Prompt 工程离不开 Context 工程你写 prompt 时必须时刻考虑上下文预算。2.2 核心技巧结构化输出与格式约束Agent 场景的 Prompt 跟聊天场景最大区别是你要求模型输出机器可读的结构而不是自然语言。常见做法是强制 JSON 输出。我常用的格式是这样的你是一个订单处理助手。当用户请求涉及查询订单时你必须调用工具 query_order。 调用工具时严格按以下 JSON 格式输出不要包含任何其他内容 {tool: query_order, args: {order_id: 从对话中提取的订单号}}注意几个细节明确“不要包含任何其他内容”。不加这句模型经常输出 “好的我将为您查询订单{...}”你的 JSON 解析直接崩。格式说明要简单到不能再简单。你给出一个例子比写三行规则有效十倍。模型是 few-shot learner不是规则解释器。如果用的是 Claude、GPT-4 这类原生支持 tool calling 的模型优先用官方的 function calling 接口而不是自己写 prompt 格式。原因很简单官方接口底层已经做了充分的格式对齐可靠性比你手写 JSON 约束高一个量级。但如果你用的是某些不支持 function calling 的开源模型自己设计 prompt 约束就是刚需。2.3 避坑Prompt 被内容安全策略拦截怎么办这是最近非常高频的报错invalid prompt: your prompt was flagged as potentially violating our usage policy很多人第一反应是“我的 prompt 政治不正确”其实不一定。触发这类拦截的往往是以下几类情况Prompt 里包含大量“如何绕过”“如何规避”“越狱”等敏感字眼即使是反面示例也会被分类器命中。你为了“测试”鲁棒性在 prompt 里写了一些暴力、色情或危险品相关词汇即便是“列举并说明这些内容为什么危险”也可能触发。你对某个知名人物、公共政策做了评价模型侧的内容防火墙直接拒掉整个请求。我的建议很简单不要在 production 代码里硬编码高危示例词。如果你确实需要模型学会拒绝有害请求用抽象的类别描述如“有害内容”“非法活动”代替具体示例。如果作为开发者测试用途你需要走合规的接口通道而不是反复提交同一段被拒绝的 prompt——这样不仅无效还可能把你的 API Key 送到更严格的风控名单里。3. Context 工程Agent 的储物间3.1 Context 不是“聊天记录”而是“可用信息边界”很多开发者的第一个 Agent 就是拿一个 messages 数组把用户所有聊天记录全部塞进去。跑到十几个回合开始报api error: the model has reached its context window limit.然后你意识到Context 是有限资源。当前主流模型的输入窗口从 128K 到 1M token 不等听起来很大但 Agent 跑几十轮之后光是把历史全部塞回去就爆炸。更麻烦的是即使不爆炸大量无关历史会稀释模型的注意力。模型不是逐字阅读所有上下文而是基于注意力机制分布聚焦夹在两千行日志中间的关键指令很有可能被忽略。所以 Context 工程的核心不是“怎么把更多内容塞进去”而是“怎么在有限的窗口里保留最关键的信息”并且用结构化的方式呈现给模型。3.2 上下文的三层架构系统层、工作层、历史层我推荐把 Agent 的上下文分成三层管理每一层单独维护、单独裁剪第一层是系统层上下文。包括角色设定、工具清单、输出格式约束、安全策略。这层基本不变每次请求都会带上但必须精简。常见问题是我见过有人把一整个产品手册塞进 system prompt这是灾难。系统层只需要“约束行为”的信息不该有具体业务数据。第二层是工作层上下文。包括当前任务的用户指令、最近一次工具调用返回的结果、当前已经完成的关键中间结论。这层是动态的每次循环变化也是最重要的。工作层的原则是“短而新鲜”只放本轮决策真正需要的事实不放大段工具返回的原始日志而是提取摘要再放回去。第三层是历史层上下文。包括更早的对话、历史工具调用、历史结果。这一层可以压缩、可以剪枝、可以滚动丢弃。我的做法是每 N 轮对历史做一次摘要化把“用户原本要什么、我们做到哪一步、还剩什么没做”压缩成几行然后丢弃原始历史。损失一部分细节换回稳定性完全值得。3.3 实操方法摘要压缩与滑动窗口的实现这块我给你一个可以直接抄作业的方案。假设你用一个 messages 数组维护上下文核心逻辑可以写成这样def build_context(messages, working_memory, max_history_blocks6): system_block messages[system] working_block working_memory # 当前任务的关键信息始终保留 history_blocks messages[history][-max_history_blocks:] if estimate_tokens(system_block) estimate_tokens(working_block) estimate_tokens(history_blocks) MAX_TOKENS: summary summarize_history(history_blocks[:-2]) history_blocks [summary] history_blocks[-2:] return system_block working_block history_blocks里面用到的summarize_history本质上是调一次模型把历史压缩成结构化摘要。很多 Agent 框架LangGraph、OpenAI Swarm 等底层都内置了这类机制但我建议你理解机制再依赖框架否则出问题都不知道去哪排查。还有一个细节注意 token 估计的误差。别把“模型最大上下文 1M token”当成“我可以随便用 1M”。不同模型 tokenizer 不同中文一个字可能对应 1-2 个 token代码一个字符也可能消耗 1 个 token。实际使用中我通常把目标输入控制在模型上限的 60% 以内因为模型的输出 token 也占用同一个窗口。你设置 max_tokens 为 8000那你输入就不能超过上限减去 8000。很多报错翻译成大白话就是你的输入 输出超出了模型窗口总容量。3.4 利器还是毒药函数调用返回值的回填策略工具的返回值是 Context 的最大吞噬者。一个数据库查询可能返回 5000 行记录你如果原样回填给模型上下文瞬间爆炸。我处理这类问题的策略是分层回填当返回结果小于 2K token直接回填。当返回结果在 2K-10K token执行摘要只回填关键字段。当返回结果大于 10K token不要回填原始数据而是落盘存储回填“文件路径 关键统计指标 读取指令”。这其实就是 RAG 的精髓不是把数据库全部交给模型而是给模型一个“知道去哪儿查”的能力。工具返回的不是数据而是数据的指针和摘要。这个习惯会让你的 Agent 续航能力成倍增长。4. Harness 工程Agent 的操作间4.1 Harness 到底是什么以及它和 Agent 的区别搜索热词里有个高频问题“harness 和 agent 区别”。我给一个通俗定义Agent 是意图Harness 是执行环境。Agent 负责决策“我应该调用哪个工具来完成下一步”Harness 负责“当 Agent 决定调用工具时环境如何把这个工具真实执行起来并把结果交回”。你搜 “deepseek harness”其实指的是围绕 DeepSeek 模型搭建的一套评测或运行容器。这个概念来自开源社区一个模型不能裸奔你需要一个 harness 把模型包起来让它能读写标准输入输出、能加载工具、能连接评测集。放在你自己的项目里Harness 就是你的执行框架层。它解决的问题如下模型输出一段文本 “我要调用 query_order”Harness 负责解析这句话映射到真实的 Python 函数。工具执行过程可能抛异常、超时、返回非法数据Harness 负责兜底处理并把这些异常转成模型能理解的自然语言反馈。工具是不是需要权限控制、是否需要人工审批Harness 负责拦截。4.2 从零实现一个轻量级 Harness工具注册与调用栈很多初学者第一步就引入 LangChain 这种重量级框架结果被抽象搞晕。我的建议是先用原生的方式手写一个 50 行 Harness感受一下工具调用到底发生了什么。核心逻辑就三块第一块工具注册表。用一个字典存放所有可用工具TOOL_REGISTRY {} def register_tool(name, description, func, parameters_schema): TOOL_REGISTRY[name] { description: description, function: func, parameters_schema: parameters_schema, }第二块工具执行器。接收模型输出的工具名和参数从注册表找到真实函数并执行def execute_tool(name, args): if name not in TOOL_REGISTRY: return {error: f未知工具: {name}请从可用工具中选择} try: result TOOL_REGISTRY[name][function](**args) return {result: result} except Exception as e: return {error: f工具执行异常: {str(e)}}注意这里的关键点异常不能直接抛出必须转换成工具返回值。因为异常一旦抛给模型模型看到的是堆栈信息它根本不知道该怎么处理。你要让异常变成模型“可以读的反馈”这样模型才知道“哦工具失败了我可以换一种方式”。第三块工具清单注入。在每次请求模型前把注册表里每个工具的 name、description、parameters_schema 自动注入到 system prompt 里。这个环节我写成自动化不手工维护def get_tools_prompt(): lines [可用工具列表] for name, meta in TOOL_REGISTRY.items(): lines.append(f- {name}: {meta[description]}参数: {json.dumps(meta[parameters_schema], ensure_asciiFalse)}) return \n.join(lines)你别小看这个简单的注册模式LangChain 里的tool、OpenAI 的 function calling 底层长得就是这个样子。理解了这三个组件你就理解了 Harness 的最小闭环。4.3 进阶Skill、MCP 与工具标准化热度很高的两个词Skill 和 MCP。Skill 本质上是“复合工具”——单个工具做一个原子操作Skill 是多个工具 prompt 模板 后处理逻辑的组合。举个例子query_order是工具而handle_refund是 Skill它内部可能依次调用query_order、verify_user、create_refund几个工具并根据中间结果决定是否继续。MCPModel Context Protocol则是一种工具接入的标准化协议。如果把工具比作插头MCP 就是统一的插座标准。接入 MCP Server 之后你的 Agent 不需要为每个数据源单独写一套调用逻辑只要遵循同一个协议即可。目前相关生态已经比较成熟你可以找到文件系统、数据库、GitHub、Slack 等各类 MCP Server。我的建议是小项目用轻量级 hand-coded tools中大型项目再上 MCP不要一上来就被协议绑架。4.4 避坑CORS、环境路径和 Daemon 类错误Harness 层最容易翻车的不只是工具逻辑还有运行环境的边缘问题。下面这几个报错我猜测每一个都在你搜索栏里出现过第一个has been blocked by cors policy: the request client is not a secure context这个发生在你在浏览器里调用 Agent API 或工具服务时。CORS 是浏览器的安全策略它阻止了非同源的 JS 请求。解决办法开发阶段用代理转发或者在后端服务里显式配置 CORS 白名单同时必须保证页面在 HTTPS 或 localhost 环境下运行HTTP 非 localhost 会被判定为 insecure context。如果你搭的是本地 Agent 工具链直接访问 localhost 即可绕开。第二个error response from daemon: get https://registry-1.docker.io/v2/: context deadline exceeded这跟 Agent 没直接关系但如果你用 Docker 来跑 Harness 运行时就会频繁遇到。Docker 拉取镜像超时本质是网络问题。解决办法配置国内镜像加速器或者设置更大的超时时间。值得注意的是很多人喜欢“通配符配置镜像加速”这个要谨慎优先选择稳定的公共镜像源。第三个command prompt 怎么在黑板上启动、anaconda prompt 下载、anaconda prompt 里面没有 opencv这些虽然偏工具链但我理解为什么会出现在你的搜索记录里——因为很多 Agent 项目依赖 Python 环境。给新手一句实在话不要用 Anaconda Navigator 去搜包学会用命令行里的 pip 和 conda并且养成用虚拟环境隔离项目的习惯。conda create -n agent python3.11这行命令解决 90% 的依赖污染问题。5. Loop 工程Agent 的呼吸循环5.1 Agent Loop 的本质感知-决策-行动-观察Agent 跟普通 API 调用的最大差异就在于它有一个循环感知Perceive从用户消息 工具返回值 历史记忆中构建当前输入上下文。决策Decide模型根据上下文决定下一步动作调用工具 / 直接回答 / 请求更多信息。行动Act执行工具调用或者生成最终答案。观察Observe将工具执行的结果作为新信息写入上下文回到第 2 步。伪代码很简单messages initial_messages while not finished: response llm.chat(messages) if response.action call_tool: tool_result execute_tool(response.tool_name, response.args) messages.append(tool_result) elif response.action answer: return response.answer else: break难点在“finished”这个条件。很多新手 Agent 变成死循环就是退出条件没写严谨。5.2 循环上限与终止条件设计无限循环是 Loop 工程最大的敌人浪费 token 还是小事卡死业务进程是大事。我在设计终止条件时至少设置以下四道防线第一道最大迭代次数。这个最简单粗暴。每轮循环计数超过 MAX_ITERATIONS比如 10 次强制终止返回当前结果或者失败信息。这是为了程序层面兜底防止一种情况模型在两个工具之间反复横跳永远不完成。我遇到过一个真实案例Agent 在调用搜索、调用总结两个工具之间来回跳了二十多次不设上限的话一次任务能消耗几十万 token。第二道明确的任务完成信号。在 prompt 里告诉模型“当你认为已经从工具结果中获取到回答用户问题所需的全部信息时不再调用工具直接给出最终答案。”这个信号必须写清楚否则模型倾向于把“调用工具”当成本能明明已经有答案了还要再调一次。第三道连续失败熔断。设定连续出现工具调用异常或无效输出达到 3 次自动终止循环进入人工处理或错误回复。这个设计能避免模型在同一个错误上反复重试浪费时间。第四道任务内容变化检测。如果模型连续多轮输出的诉求发生了明显漂移比如用户问订单模型开始聊天气立即终止并回退到初始意图。这部分实现稍复杂很多框架现在用“意图一致性检查器”来完成属于进阶玩法。5.3 事件循环与 asyncio 的坑技术热词里有一类跟“Loop”关系很大但常被误解例如“js event loop 过程”“asyncioscheduler(looploop)”。这里我说一个 Agent 开发中非常常见的 Python 异步问题。很多 Agent 的 Harness 层是异步的但你的工具函数可能是同步的混用会出现这样的报错RuntimeError: asyncio.run() cannot be called from a running event loop或者asyncioscheduler(looploop) 错误loop is already running解决方案很直接使用asyncio.to_thread或run_in_executor把同步工具跑在线程池里不要把asyncio.run()嵌套调用在另一个协程里面。给你一个可复用的包装器import asyncio async def run_sync_tool(func, *args, **kwargs): return await asyncio.to_thread(func, *args, **kwargs)你注册的每个工具函数统一走这个包装器就不会陷入 event loop 嵌套调用的泥潭。这是我在接大量异步 Agent 框架时最深刻的一条教训框架给你的 async 只是表皮你的工具不一定 async 就绪。另外如果是前端背景的读者做 Agent UI遇到 “js event loop” 相关的问题记住了setTimeout 不能阻塞执行、await 后面要跟真正的 Promise、不要在循环里同步调用高延迟 API。前端到后端的事件流最好通过 WebSocket 或者 SSEServer-Sent Events推送而不是用轮询。这块做不好你的 Agent 页面会非常卡。5.4 Human-in-the-Loop人机协同的循环节点热搜词 “使用 langgraph 或 langchain 实现 human in the loop” 指向了一个重要设计不是所有步骤都应该让模型自己拍板。涉及敏感操作转账、删除、发送邮件、高风险判断医疗建议、法律结论或者模型置信度低的场景必须在 Loop 里插入人工审批节点。实现思路非常清晰当模型决定执行某操作时Harness 拦截调用改为发送确认请求给用户通过 UI 通知、IM 消息、邮件等待用户确认后才真正执行。在程序层面这只是一个条件断点if requires_human_approval(tool_name, args): approved await request_human_approval(tool_name, args) if not approved: return {error: 用户拒绝了该操作}在 LangGraph 里这对应interrupt()机制在 LangChain 里则可以通过自定义回调实现。我的实际经验是宁可多设几个人工确认点不要迷信全自动。全自动的 Agent 跑出一次不可逆事故比慢 30 秒严重得多。6. 常见问题与排查技巧实录前面每一层都穿插了一些报错分析这里我把搜素热度最高的几类问题整理成速查表方便你在开发时快速定位。6.1 报错速查表错误信息或现象所属层次根因方向快速处理建议prompt is too long / maximum context lengthContext输入超限压缩历史、截断工具返回、减少 system promptinvalid prompt: flagged as potentially violating usage policyPrompt内容安全拦截检查 prompt 中的敏感示例词改用抽象描述the model has reached its context window limitContext输入输出超限降低 max_tokens或启用窗口滑动ran out of room in the models context window. start a new threadContext对话过长开启新线程或把历史摘要化后重新开始tool 反复调用 / 死循环Loop终止条件缺失加最大迭代次数明确完成信号asyncio.run() cannot be called from a running event loopHarness/Loop异步嵌套错误用 asyncio.to_thread 包装同步工具has been blocked by cors policyHarness浏览器跨域配置后端 CORS 白名单开发用代理error response from daemon: context deadline exceededHarness网络拉镜像超时配置镜像加速增大超时时间6.2 一个典型的“Agent 跑飞”排查案例拿一个我最近帮朋友排查的真实场景举例。现象是Agent 在第四轮循环后回答完全跑偏从查询订单变成了推荐电影。我们一步一步看第一步查 Prompt 层。system prompt 没问题用户新指令也明确。但查看完整上下文发现第三轮工具返回了一个“猜你想看”的推荐位数据里面全是电影名。这个数据混进了工作层上下文模型误以为用户要求推荐电影。排查结论工具返回值的回填策略出了问题。订单查询接口返回的完整页面数据里包含了广告位推荐不应该全部回填给模型。把工具返回改成只提取订单关键字段问题立刻消失。这个案例说明一件很重要的事Agent 跑飞不一定是模型“变傻”了而是你的上下文里混入了不该出现的垃圾信息。Context 工程管的就是这件事它不是可选项而是刚需。6.3 一个“Harness 工具永远失败”的排查案例另一个高频场景Agent 调用的工具明明单独测试没问题但放进 Agent 里就报错。最后发现工具函数的参数名和模型生成的参数名对不上。模型按 prompt 中的参数描述生成了orderId而你的 Python 函数签名是order_id导致execute_tool调用func(**args)时抛TypeError: unexpected keyword argument orderId。解决办法有两个要么在参数 schema 里明确标注“参数名必须使用下划线风格”要么在工具执行器里做一层参数名映射。我习惯的做法是在注册工具时加一个arg_name_map配置把常见的大小写/命名风格变体统一映射到真实参数名。这种细节框架不会替你解决。7. 结尾一点个人体会最后分享两个我反复跟团队强调的经验。第一把每一层都做“可观测”。Prompt 层要能记录每次发送给模型的完整消息Context 层要能计算每轮 token 消耗和裁剪策略触发情况Harness 层要能把每次工具调用的输入输出落日志Loop 层要能追踪迭代次数和每次决策的理由。没有这个你排查问题全靠猜效率极低。很多框架自带 trace 功能但如果你是自己手写的体系务必预留日志埋点。第二别一开始就追求“全自动”。先让 Agent 每一步都向用户确认跑通了、稳定了再逐步放开自动执行的边界。这个“渐进信任”的思路能帮你避开大量生产事故。Agent 的工程体系不是什么神秘的高深理论拆开就是这四块把话说清楚Prompt把材料整理好Context把手脚接上Harness把节奏控住Loop。四件事都不难难的是你愿意静下心把每一件都做到位。希望这篇拆解能让你少走几段弯路。
返回列表