尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

上下文工程与LLM Harness:让Agent稳定跑完复杂任务

上下文工程与LLM Harness:让Agent稳定跑完复杂任务 如果同一批大模型 API 放在两个团队手里一个团队做出来的 Agent 面对稍微复杂的任务就会失控另一个团队却能让同样的大模型稳定跑完查询、分析、调用工具、生成结论的全流程差距到底在哪里答案通常不在模型本身而在模型输入的那段上下文以及承载这段上下文的工程框架。模型能力被 API 拉开的空间越来越小真正决定应用上限的是你怎么构造上下文、怎么控制 token 预算、怎么让工具调用结果回到对话流里、怎么把一次次成功经验沉淀成可复用的技能。这个领域现在有一个专门的叫法Context Engineering上下文工程。而让上下文工程能够稳定落地、可配置、可观测、可回滚的载体就是 LLM Harness。这篇文章会用一套最小可运行的思路把 Context Engineering 和 Harness 的关系讲清楚并给出可以直接改用的 Python 示例和配置骨架。读完之后你能回答三个问题为什么普通 Prompt 工程不够用、一个 Harness 的内部模块长什么样、以及在自己的项目里应该先从哪里动手。1. 这篇文章真正要解决的问题很多 LLM 项目都卡在同一个地方Demo 跑通很容易Production 稳住很难。刚接入 API 时单次对话效果不错但一旦接上 RAG、工具调用、多轮记忆问题就开始冒头。常见的现象是Prompt 越写越长从 500 字写到 3000 字效果反而下降。RAG 检索出 10 段文档模型被无关信息带偏。工具调用返回了结果但模型没有正确引用依然给出错误回答。多轮对话之后历史消息把上下文窗口塞满API 直接报错。同一套业务逻辑散落在不同函数里想调一个参数要改好几个文件。这些问题的共同点是上下文没有被当成一个“工程对象”来管理。普通 Prompt 工程把注意力放在“怎么写指令”上但在真实 Agent 场景里模型吃的不是一段 Prompt而是一整套动态组装出来的消息序列系统约束、用户输入、检索片段、工具 Schema、历史记忆、中间执行结果。这套序列怎么组装、按什么顺序、哪些优先、超了怎么办就是 Context Engineering 的范畴。所以本文不是教你“写一个更好的 Prompt”而是教你把上下文组装、工具执行、技能沉淀放进一个可维护的 Harness 框架里。适合正在做 LLM 应用开发、Agent 开发或者想从纯 API 调用者升级为应用架构者的读者。2. Harness 与 Context Engineering先分清三组概念2.1 什么是 LLM HarnessHarness 直译过来是“挽具”或“控制装置”。在 LLM 应用里Harness 指的是包裹在模型外围、负责控制模型输入输出、调度工具、管理会话状态、记录运行轨迹的那一层工程框架。如果把大模型比作一台高性能发动机Harness 就是整车的控制系统油门怎么踩、刹车什么时候介入、仪表盘显示什么、出现异常怎么降级。它不替代模型但决定了模型在真实任务里能不能安全、稳定地发挥作用。近期社区里 Karpathy 提出的 LLM Wiki 范式以及很多团队在讨论的 Harness 工程本质上都是在做同一件事不要每次把上下文临时拼出来而是把知识、技能和策略沉淀成结构化的资产再由 Harness 在运行时按需取用。2.2 什么是 Context EngineeringContext Engineering 的定义可以这样理解设计、构造和维护模型上下文的全过程。它不只包含 Prompt 文本还包括系统提示词怎么分层、怎么写。Few-shot 示例选哪些、放多少。RAG 检索出来的文档怎么截断、怎么排序。工具描述用什么格式才能被模型稳定解析。用户输入和检索内容如何拼接谁先谁后。多轮历史记录保留多少轮、哪些信息需要压缩。上下文总长度超过窗口时优先砍掉哪部分。这些决定直接影响了模型输出的质量。同一个模型上下文构造合理的时候任务完成率能拉开非常大构造混乱的时候再强的模型也会给出让人哭笑不得的结果。2.3 容易混淆的三个误区第一个误区Context Engineering 就是写 Prompt。实际上写 Prompt 只是上下文工程中最浅的一层。优秀上下文工程要处理的是动态输入、多源拼接和预算控制这更像在设计数据管线。第二个误区Harness 就是 API 封装。只封装 API 是调用层的事Harness 还要负责流程编排、工具注册、上下文构建、观测和回滚。它是一套运行系统不是一个 SDK 壳子。第三个误区上下文越长越好。这在实际业务里非常危险。上下文窗口是有限的即便窗口很大模型对中间内容的注意力也会衰减。真正重要的是让最关键的信息出现在最合适的位置。2.4 小结论Harness 给出了一个结构Context Engineering 提供内容。没有 Harness上下文策略再优秀也很难工程化复用没有 Context EngineeringHarness 就只是个空壳。两者组合起来才是大模型应用工程化的关键。3. 为什么普通 Prompt 工程不够需要 Harness 化先看一组概念对比。维度Prompt EngineeringContext EngineeringHarness Engineering核心关注指令文本的写法上下文的组装与预算整套运行框架控制范围单次输入单次/多次/Agent任务全生命周期复用粒度文本模板上下文策略模块配置技能库可观测性难中高典型产出更好的系统提示词Context Builder可部署的Agent框架从这张表能看出来Prompt Engineering 是 Context Engineering 的一个子集而 Context Engineering 又需要在 Harness 里才能真正发挥威力。看一个具体例子。假设你要做一个客服 Agent模型需要查询订单状态并且能根据订单是否可退款给出建议。最简单粗暴的写法是response llm.chat( f你是客服助手请查询订单 {order_id} 的状态并给出建议。 )这条 Prompt 问题很多。模型不知道订单系统怎么调用不知道检索文档在哪里也不知道多轮状态下用户之前说过什么。随着业务复杂你会往里面拼更多字符串最后变成一大坨难以维护的文本。Harness 化之后的流程是这样的系统提示词从独立文件加载。用户输入进入 Context Builder。Context Builder 从检索组件拿到相关资料从工具注册表拿到工具 Schema。所有片段按预算策略组装成 messages。Harness 调用模型。如果模型返回工具调用Harness 执行工具把结果追加回消息。最终输出答案并记录日志。这一步的变化是根本性的上下文不再是“现拼字符串”而是由模块负责组装并且每一步都可以观测、可以测试、可以回滚。这就是 Harness 化的价值。4. 一个最小 Harness 应该包含哪些模块刚开始做 Harness 时不要一上来就搞微服务、MQ、分布式链路追踪。先做最小闭环跑通单 Agent 主链路。下面这些模块是必须的。模块职责缺失时的后果Context Builder组装 messages控制 token 预算Prompt 混乱token 溢出工具注册表注册、校验、调用外部工具Agent 无法稳定使用工具控制器决定是否继续调用工具还是输出答案Agent 死循环或提前结束记忆存储保存多轮会话状态Agent 失忆任务中断观测日志记录每次请求的上下文和调用轨迹出错时无法排查对应地可以先用一份 YAML 配置把主要参数集中起来# harness-config.yaml project: customer-service-agent model: provider: openai-compatible model_name: gpt-4o-mini temperature: 0.2 max_tokens: 1024 context: max_context_tokens: 8192 reserve_output_tokens: 1536 system_prompt: prompts/system.md skills_dir: skills/ rag: top_k: 3 max_section_chars: 1200 tools: - name: search_order enabled: true timeout_seconds: 10 - name: refund_order enabled: false requires_human_approval: true memory: type: in-memory max_conversation_turns: 10 observability: log_level: INFO这里的核心思想是把“能和模型对话的内容”全部交给 Context Builder 去动态生成。system_prompt指向独立文件方便版本管理skills_dir用来存放沉淀下来的技能模板memory控制历史轮次tools的enabled和requires_human_approval直接决定了 Agent 的工具权限边界。这些配置项不是写死的一次性参数而是整个 Harness 的稳定接口。5. 完整示例用 Python 实现一个轻量 Harness下面这套示例不绑定某个具体商业产品只依赖 Python 3.9 以上基础库用来演示 Harness 如何把上下文组装和工具执行串起来。你可以在本地跑通后再把 LLM 调用替换成线上 API。5.1 目录结构harness_demo/ ├── harness-config.yaml ├── context_builder.py ├── harness.py ├── main.py └── prompts/ └── system.md5.2 Context Builder负责组装上下文# context_builder.py from dataclasses import dataclass from typing import Optional dataclass class ContextPlan: system: str memory: list retrieval: str tools: str messages: list estimated_tokens: int def estimate_tokens(text: str) - int: # 仅用于示例生产环境建议使用模型配套的 tokenizer return max(1, len(text) // 3) class ContextBuilder: def __init__(self, config: dict): self.config config self.max_context_tokens config[context][max_context_tokens] self.reserve_output_tokens config[context][reserve_output_tokens] self.budget self.max_context_tokens - self.reserve_output_tokens def load_system_prompt(self) - str: path self.config[context][system_prompt] with open(path, r, encodingutf-8) as f: return f.read().strip() def build_messages( self, user_input: str, history: Optional[list], rag_sections: list, tool_schemas: list, ) - ContextPlan: system self.load_system_prompt() max_turns self.config[memory].get(max_conversation_turns, 10) memory history[-max_turns:] if history else [] retrieval_text \n\n---\n\n.join(rag_sections) tool_text \n.join(str(schema) for schema in tool_schemas) user_content ( f用户输入{user_input}\n\n f参考资料\n{retrieval_text}\n\n f可用工具\n{tool_text} ) messages [{role: system, content: system}] messages.extend(memory) messages.append({role: user, content: user_content}) estimated sum(estimate_tokens(m.get(content, )) for m in messages) return ContextPlan( systemsystem, memorymemory, retrievalretrieval_text, toolstool_text, messagesmessages, estimated_tokensestimated, )这段代码的关键点是Reserve output tokens是从上下文预算里提前扣出来的避免模型回答到一半被截断。memory只保留最近 N 轮避免对话历史无限膨胀。用户输入里明确区分了“用户输入”“参考资料”“可用工具”三块让模型知道每一段的用途。5.3 Harness编排工具调用循环# harness.py import json import re from context_builder import ContextBuilder def extract_action(text: str) - dict: if json in text: text text.split(json, 1)[1].split(, 1)[0] match re.search(r\{.*\}, text, re.S) if not match: return {} try: return json.loads(match.group()) except json.JSONDecodeError: return {} class ToolRegistry: def __init__(self): self._tools {} def register(self, name: str, func, schema: dict): self._tools[name] (func, schema) def schema_list(self): return [schema for _, schema in self._tools.values()] def call(self, name: str, **kwargs): if name not in self._tools: raise ValueError(ftool not found: {name}) func, _ self._tools[name] return func(**kwargs) class Harness: def __init__(self, config: dict, llm_client): self.config config self.builder ContextBuilder(config) self.registry ToolRegistry() self.llm_client llm_client self.memory [] self.max_iters 5 def run(self, user_input: str, rag_sections: list): messages None for iteration in range(self.max_iters): context_plan self.builder.build_messages( user_inputuser_input, historyself.memory, rag_sectionsrag_sections, tool_schemasself.registry.schema_list(), ) messages context_plan.messages print(f[harness] iteration{iteration} estimated_tokens{context_plan.estimated_tokens}) resp self.llm_client(messages, temperatureself.config[model][temperature]) action extract_action(resp) if action not in action: print([harness] final answer:, resp) return resp tool_name action[action] params action.get(params, {}) print(f[harness] call tool: {tool_name} params{params}) tool_result self.registry.call(tool_name, **params) print(f[harness] tool result: {tool_result}) messages.append({role: assistant, content: resp}) messages.append({role: tool, content: json.dumps(tool_result, ensure_asciiFalse)}) self.memory messages raise RuntimeError(max iterations reached without final answer)这里用了一个轻量的办法让模型在需要调用工具时返回 JSON 格式的动作声明Harness 解析后执行工具再把工具结果追加回消息列表。这种方式不依赖特定厂商的 function calling 协议适合理解和迁移。真实项目里如果使用某个成熟 SDK可以直接把这段换成原生工具调用参数思路完全一致。需要特别注意的一点工具结果的追加顺序必须是assistant消息在前tool结果在后。很多新手会把工具结果直接塞进用户消息导致模型分不清哪些是用户说的、哪些是执行结果回答质量会明显下降。5.4 main.py组装并运行# main.py import json from harness import Harness def load_config(path: str) - dict: with open(path, r, encodingutf-8) as f: return json.load(f)这里先说明一下上面的load_config只是示意。因为 YAML 解析需要pyyaml为了让你能直接跑下面用 JSON 版的配置演示避免额外安装依赖。# main.py (完整版) import json from harness import Harness def mock_llm(messages, **kwargs): last_content messages[-1][content] if 查订单 in last_content: return json.dumps( {action: search_order, params: {order_id: A1001}}, ensure_asciiFalse, ) return 订单 A1001 已发货预计明天送达。 def search_order(order_id: str): return {order_id: order_id, status: shipped, eta: tomorrow} def main(): config { model: {temperature: 0.2}, context: { max_context_tokens: 8192, reserve_output_tokens: 1536, system_prompt: prompts/system.md, }, memory: {max_conversation_turns: 10}, } harness Harness(config, llm_clientmock_llm) harness.registry.register( search_order, search_order, {type: function, name: search_order, description: 查询订单状态}, ) rag_sections [订单 A1001 当前状态已发货物流信息更新于 10 分钟前。] harness.run(帮我查一下订单 A1001 的状态, rag_sections) if __name__ __main__: main()运行之前还需要在prompts/system.md里放一份系统提示词比如你是一个客服助手。当用户需要查询订单时你需要调用 search_order 工具。 如果不需要调用工具直接回答用户问题。 工具调用请严格输出 JSON 格式例如{action: search_order, params: {order_id: A1001}}然后执行python main.py预期输出大致是[harness] iteration0 estimated_tokens420 [harness] call tool: search_order params{order_id: A1001} [harness] tool result: {order_id: A1001, status: shipped, eta: tomorrow} [harness] final answer: 订单 A1001 已发货预计明天送达。这个最小示例跑通后你就有了一个 Harness 的概念骨架。接下来所有的优化都可以往这个骨架里加模块而不是改业务代码。6. 进阶方向Meta Context Engineering 与 Agentic Skill Evolution如果说前面介绍的是“手工构造上下文”那么社区最近讨论的 Meta Context Engineering 就是在往“让上下文策略自己进化”的方向走。这个方向也出现在 Agentic Skill Evolution 这类热词里。Meta Context Engineering 的重点不是针对某一次任务设计上下文而是设计一套机制让 Agent 在完成大量任务后自动总结经验生成新的 Skill反过来优化后续任务的上下文策略。一个可落地的思路是这样的Harness 记录每一次成功任务的上下文片段和工具调用序列。从大量成功案例中提取规律形成新的技能模板。技能模板经过评估集筛选后注册进 Harness 的技能库。后续任务触发相似场景时Context Builder 自动加载对应技能。举个例子。如果你的客服 Agent 反复处理“订单查询→物流查询→催发货”的问题系统可以沉淀出一个名为order_pursuit的 Skill保存这段任务的上下文组装策略和工具调用顺序。下次遇到类似请求Context Builder 就会优先装载这个 Skill而不是从头开始组装。# skills/order_pursuit.yaml name: order_pursuit description: 处理订单查询和催发货场景 trigger: - 查订单 - 催发货 context_template: system_prompt: prompts/system.md rag_weight: 0.6 tool_order: - search_order - track_shipping max_history_turns: 8 evaluation: pass_rate_threshold: 0.85这个方向有两个很大的价值一是让团队的经验能够结构化管理不随人员流动而流失二是减少每次迭代都需要人工改 Prompt 的工作量。至于究竟能否全部让智能体自动演进社区还没形成统一结论但从“手工配置”走向“半自动生成技能”的趋势是清晰的。要注意的是Skill 自动生成并不等于无监管更新。任何新技能在进入生产 Harness 之前都需要经过评估和审批。否则一次错误的技能沉淀会让整条上下文策略退化而且很难定位问题。7. 运行结果与效果验证跑通示例只是第一步更重要的是建立一套“怎么判断 Harness 变好还是变坏”的验证方式。在最小 Harness 里建议至少观察三类数据。第一类是日志。每条请求都应该输出{ request_id: req_0001, estimated_tokens: 420, tool_calls: [ {name: search_order, params: {order_id: A1001}} ], final_answer: 订单 A1001 已发货预计明天送达。, duration_ms: 812 }第二类是效果指标。针对同一批测试用例记录任务完成率、工具调用成功率、上下文截断次数。任务完成率是最重要的指标单独跑一两条 Prompt 看不出问题必须放到固定数据集里看变化。第三类是稳定性指标。同一个任务重复跑 5 次输出的关键结论是否一致。如果每次结果都不一样可能是上下文里缺少约束或者温度参数太高。如果运行失败第一步不要改 Prompt先看日志里这四件事messages 是否完整有没有被截断。estimated_tokens 是否超过预算。工具调用是否执行成功。工具结果是否成功追加回 messages。按照这个顺序排查大多数问题都能定位到具体环节。8. 常见问题与排查思路问题现象可能原因排查方式解决方案模型回答被截断输出 token 超出 max_tokens或预留不足查看 usage 和日志中的 token 数据调大 max_tokens或在 Context Builder 中预留更多输出空间模型没有引用工具结果工具结果未追加到 messages检查 Harness 日志中的工具调用记录确认 tool role 消息在 assistant 消息之后写入Agent 反复调用同一个工具缺少终止条件或模型没有消费结果查看调用次数和最终 action设置 max_iterations工具返回后要求模型做摘要判断上下文内容过杂导致效果下降RAG 段落和工具描述堆砌过多查看 estimated_tokens 和内容来源限制 top_k 和 max_section_chars让关键信息靠前同一任务多次结果不一致温度过高或缺少确定性约束在固定数据集上多次运行温度设为 0 到 0.3输出格式用结构化约束工具调用权限过大工具 Schema 暴露了敏感操作审计工具注册表默认关闭高权限工具申请后审批打开在这些问题里最隐蔽的是第三个Agent 反复调用同一个工具。表面看是模型变笨了实际往往是因为工具结果已经返回但上下文策略没有告诉模型“接下来怎么判断”。这时需要在系统提示词里加一条拿到工具结果后必须先判断任务是否完成再决定是否继续调用工具。9. 最佳实践与工程建议9.1 把上下文预算当成硬约束每个模型都有上下文窗口但窗口不等于都可以给模型“自由发挥”。在 Context Builder 里要同时考虑输入 token 和输出 token 两部分的预算。建议把预留输出 tokens 单独配置并让上下文上限低于模型硬上限给后续扩展留缓冲。9.2 上下文组件必须版本化系统提示词、RAG 截断策略、工具 Schema、Skill 文件都应该像代码一样纳入版本管理。每次改动走评审改完之后用固定测试集回归。不要直接在生产环境改 Prompt哪怕只是在原来的末尾加一句话也可能影响全量任务。9.3 可观测性要前置Harness 的每个关键节点都要有结构化日志上下文组装、模型调用、工具调用、结果回写。不要等到线上出问题再补日志。使用 JSON 格式输出日志方便后续接入日志平台和链路追踪。9.4 安全边界要显式化工具注册表默认关闭高权限操作。对“退款、删数据、发消息”这类工具必须要求人工审批。即使某个 Agent 已经通过了测试也不能让它拥有无限权限。社区讨论里经常提到的 excessive agency 风险指的就是模型被提示注入或误判时能操作超出任务需要的敏感资源。Harness 的requires_human_approval配置就是用来兜底的。9.5 从单个任务场景开始迭代建议团队不要一开始就做一个通用 Agent 平台而是选定一个高频、边界清晰的业务场景先跑通最小 Harness记录完整日志再逐步抽象通用模块。越早把 Context Engineering 的流程固定下来后续的收益越明显。10. 总结与后续学习方向这篇文章的主线是从一个实际痛点出发模型能力接近应用效果却有天壤之别真正拉开差距的地方在于 Context Engineering 的成熟度。而让上下文工程落地的前提是有一个能承载上下文组装、工具调度、记忆管理和日志观测的 LLM Harness。文中给出了一套最小架构和可运行代码但更重要的是建立了一种思维方式上下文不是一个静态字符串而是一条由系统、记忆、检索、工具 Schema、执行结果共同组成的动态管线。你可以先照着示例跑通单 Agent 主链路然后逐步加入 RAG、Skill 库和评估集。如果今天只做一件事建议先做上下文预算管理。找一个现有 Agent 项目把所有拼字符串的地方收敛到 Context Builder加上 token 估算和日志输出。这个动作做完后续的优化就有了稳定底座。再往后值得深入的方向有三个一是 RAG 和上下文的深度结合比如检索重排、上下文压缩二是 Agentic Skill Evolution尝试把成功经验沉淀为可持续演化的技能库三是 Harness 的工程化能力包括灰度发布、回滚、监控和对账。Context Engineering 不是一个一次性的 Prompt 技巧而是大模型应用开发工程师需要长期打磨的核心能力。
返回列表