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

资讯详情

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

上下文工程与LLM Harness:掌控LLM应用质量的工程密钥

上下文工程与LLM Harness:掌控LLM应用质量的工程密钥 我们先用一个很常见的翻车场景开场。你的应用接入了目前能力很强的 LLM模型的上下文窗口也从 128K Token 一路卷到了 1M Token可一到真实项目里问答还是答非所问Agent 执行到第三步就开始原地绕圈RAG 检索出来的资料明明是对的模型却把它当成噪音忽略掉。很多人第一反应是换更大的模型结果成本上去了问题还在。真正的瓶颈往往不在模型本身而在你喂给模型的那一段上下文是怎么被组装、调度、压缩和回填的。这就是这篇文章要讨论的核心主题Context Engineering上下文工程与 LLM Harness。我先把结论放在前面决定 LLM 应用体验上限的已经不是单一模型能力而是上下文质量与编排方式。Harness 是承载上下文工程的工程载体它决定了一段用户输入最终变成什么样的一批 Token 进入模型也决定了工具调用结果如何回到对话流里。理解了 Harness 里的上下文生命周期你才能真正掌控长窗口、Agent、RAG 这些看似复杂的问题。文章会从概念边界讲起再拆解 Harness 的上下文流转架构然后用一个最小可运行的 Python Harness 示例带你走通“上下文组装—预算控制—工具回填—循环终止”的完整链路。最后给出常见问题排查表和工程建议。无论你是正在写 RAG 应用、做 Agent 编排还是在选型 LLM 框架这篇文章都能帮你建立一套更清晰的判断标准。1. 为什么 Context Engineering 与 LLM Harness 值得关注先说一个容易被忽视的事实上下文窗口变大并不等于上下文质量变好。模型能看到更多 Token只是意味着容量上限提高了但如果你把一堆互相冲突的指令、过期资料、重复历史消息塞进窗口模型照样会产生幻觉、遗忘关键约束甚至被检索出的无关内容带偏。容量是模型给的而容量怎么用是应用层决定的。Context Engineering上下文工程要解决的就是“怎么用”这个问题。它关注的不是某一句提示词怎么写而是整个请求里所有信息的结构化组织方式系统指令占多少空间用户问题放在什么位置检索结果以什么顺序插入历史对话截断到哪一轮工具调用结果用什么格式回填。这些决策单独看都很小组合起来却直接决定模型输出质量。Harness 则是把上下文工程落地的工程层。你可以把它理解成一个“调用控制器”它负责接收用户输入调用检索和记忆模块组装最终请求管理模型返回的工具调用再把工具结果放回上下文继续下一轮。社区里常见的 Agent 框架、编排框架、甚至最近大家讨论热度很高的 DeepSeek Harness、LLM Wiki 这类项目本质上都在解决同一个问题——让上下文按照可控的方式流转。这也是“LLM 应用为什么需要编排框架”这个问题的答案没有编排上下文就是一团乱麻。什么样的读者最应该读这篇文章如果你正在做 LLM 应用开发写过几个 Prompt 但发现效果不稳定如果你在做 RAG 但检索结果经常“召回了却用不上”如果你在调试 Agent 多步工具调用时觉得上下文越来越不可控那么这篇文章恰好覆盖了这些问题。文章不会给你一个万能框架而是帮你建立一套在任何框架下都适用的上下文管理方法论。2. 核心概念Context Engineering、Harness 与 Agent 的边界很多刚接触 LLM 应用开发的人会把“提示工程”和“上下文工程”混为一谈。这两个概念确实相关但关注点完全不同。提示工程Prompt Engineering关注的是单次请求里的指令表达怎么把问题问清楚怎么给示例怎么约束输出格式。它默认请求里的信息是静态的、可控的。上下文工程Context Engineering则面向动态系统信息来自检索、记忆、工具调用、多轮历史信息量会变化顺序会影响结果Token 预算需要动态分配。一句话概括提示工程是写好一段话上下文工程是管理一整份动态材料。Harness 这个词在 LLM 语境下指代的是把模型调用、工具调度、上下文管理封装起来的运行时层。它类似于汽车里的线束Wiring Harness把各个电气部件连接起来并保证信号按预期路径传输。在 LLM 应用里Harness 就是把用户输入、检索结果、工具结果、模型输出这些“信号”按正确路径组织起来的控制层。Harness 工程Harness Engineering也随之成为一个新话题强调的是把模型调用做得可观测、可控制、可回滚。Agent智能体则是 Harness 之上的一种应用形态。Agent 的核心能力不是单次回答而是通过多轮“思考—调用工具—观察结果—再思考”的循环完成复杂任务。Agent 必须有上下文管理能力因为每一轮工具调用结果都会追加到上下文里没有管理和压缩Agent 很快就会触顶或迷失。可以这样理解三者的关系Context Engineering 是方法论Harness 是工程载体Agent 是上层应用。没有好的 HarnessAgent 只是在一堆越来越乱的 Token 里碰运气。概念解决什么问题典型载体常见误解提示工程单次请求的指令表达Prompt 模板很多人把它当成上下文工程的全部上下文工程管理进入模型的全部信息Harness、RAG、记忆策略以为只是把资料塞进窗口Harness 工程编排调用链路与上下文生命周期Agent 运行时、编排框架以为只有大厂才需要Agent用 LLM 完成多步任务工具调用循环以为 Agent 是单独的模型3. Harness 中的上下文流转架构与生命周期在具体写代码之前我们需要先建立一张“上下文流转地图”。一个典型的 LLM Harness 至少包含五个核心组件。第一模型客户端Model Client负责与模型 API 通信包括认证、重试、超时处理。第二上下文组装器Context Assembler负责把系统指令、用户输入、检索内容、历史消息、工具 Schema 按既定策略拼装成最终请求体。第三检索与记忆模块Retriever / Memory负责从外部知识库或历史会话中取回相关信息。第四工具注册表Tool Registry负责管理可被模型调用的函数列表、参数 Schema 和执行器。第五策略控制器Policy Controller负责预算分配、截断策略、循环终止条件。这五个组件协同工作形成了上下文的标准生命周期用户输入 │ ▼ ┌────────────────────────────────────────────┐ │ Harness 运行时 │ │ 1. 意图解析与输入清洗 │ │ 2. 检索 / 记忆召回调取 │ │ 3. 上下文组装与预算控制 │ │ 4. 模型调用含工具 Schema │ │ 5. 工具结果回填与上下文压缩 │ │ 6. 循环 / 终止判断 │ └────────────────────────────────────────────┘ │ ▼ 最终回复这里最关键的设计决策是 Token 预算分配。很多开发者的做法是“把能塞的都塞进去”等报错说超过上下文限制时再简单粗暴地截断最早的消息。这种做法会让系统行为非常不稳定。更稳妥的做法是先确定一个总预算Total Budget再按固定比例分配给各组成部分并且永远预留一部分空间给模型输出和工具结果回填。上下文组成部分建议预算比例说明系统指令System10% ~ 15%固定指令尽量精简少样本示例Few-shot5% ~ 10%按需携带不常驻历史消息History15% ~ 20%滚动窗口保留最近轮次检索内容Retrieval25% ~ 30%RAG 的核心材料按相关性截断模型输出Output10% ~ 15%预留回答空间防止中途截断保留空间Reserved10% ~ 15%应对工具结果回填和不可预知开销上面表格里的比例不是金科玉律而是一个可调整的起点。不同任务类型差异很大纯问答系统可以调高检索比例多轮客服系统可以调高历史比例复杂 Agent 任务则需要给工具结果回填留更多余量。真正重要的是“预算意识”即每一部分都有上限而不是无限膨胀。4. 环境准备与前置条件下面进入实操环节。我们先准备一个最小可运行的环境。本文示例使用 Python 3.10 及以上版本版本号请以你本机实际环境为准。核心依赖包括 OpenAI 兼容的 API 客户端、Token 计数库 tiktoken、配置解析库 PyYAML。无论你使用的是 OpenAI 的官方接口还是兼容 OpenAI 协议的其他模型服务这套代码结构都可以直接迁移。# 建议使用虚拟环境 python -m venv .venv source .venv/bin/activate # 安装依赖版本以实际环境为准示例使用常见稳定版本 pip install openai tiktoken pyyaml如果你的模型服务不是 OpenAI 官方接口而是国内厂商提供的 OpenAI 兼容接口一般在部署文档里会给出 Base URL 和 API Key 配置方式。为了方便本地调试我们通过环境变量来传递这些敏感信息避免把密钥写死在代码或配置里。# Linux / macOS export OPENAI_API_KEY你的 API Key export OPENAI_BASE_URL你的服务地址例如 https://api.example.com/v1 # Windows PowerShell # $env:OPENAI_API_KEY 你的 API Key # $env:OPENAI_BASE_URL 你的服务地址需要注意不同模型系列的 Token 计数方式存在差异。tiktoken 的encoding_for_model方法对 OpenAI 自家模型支持较好对第三方模型未必能准确匹配。如果你使用的是非 OpenAI 模型更稳妥的做法是以服务提供商返回的 usage 字段为准或者使用服务商提供的 Tokenizer 工具。这一点在排查问题时特别容易踩坑我们会在常见问题章节展开。5. 核心流程拆解从输入到输出六个关键环节环境准备好之后我们来拆解 Harness 内部的处理流程。这六个环节是上下文管理的骨架掌握了它们你就能理解市面上绝大多数编排框架的设计逻辑。5.1 意图解析与输入清洗第一环是对用户输入做基础清洗。用户输入可能包含多余换行、无意义字符、超长文本甚至与当前任务完全无关的内容。Harness 在这一步需要判断输入是否需要进入检索流程是否需要触发工具调用还是仅仅是一次普通问答。这个判断可以由规则完成也可以由模型完成。实际项目中这一步做得越轻越好不要把简单问题复杂化。5.2 检索与记忆召回如果是 RAG 应用第二步就是从向量数据库或知识库中召回相关内容。这里要记住一个原则检索是在组装上下文之前完成的而且检索结果需要“先过滤再注入”。很多系统直接把 top_k 条结果全部拼接进 Prompt结果引入大量噪音。更好的做法是按相关性设定阈值只保留与问题明显相关的片段并且控制单条片段的长度。5.3 上下文组装与预算分配第三步是核心中的核心。Harness 按照预算比例把系统指令、少样本示例、历史消息、检索内容按顺序组装起来。顺序设计有讲究系统指令在最高层用来定调历史消息紧随其后保持对话连贯检索内容放在用户问题之前作为参考资料。如果检索内容放在系统指令之前模型的注意力会被资料带偏导致指令约束失效。这是新手最容易犯的错误之一。5.4 工具调用与结果回填当模型判断需要调用工具时会返回 tool_calls 字段而不是直接返回最终答案。Harness 需要解析这个字段找到对应的工具执行器传入模型提供的参数然后把工具执行结果以 roletool 的消息回填到上下文。回填格式必须规范否则模型无法正确理解结果属于哪个工具调用。另外工具参数是模型生成的 JSON必须做严格的格式校验和合法性检查不能直接信任。5.5 上下文压缩与截断每一轮工具调用都会向上下文追加 token如果不做控制几轮之后就会触顶。压缩策略有很多种最简单的策略是“滚动窗口”只保留最近 N 轮消息进阶策略是“摘要压缩”把较早的对话用模型提炼成一段摘要再进阶的策略是“关键信息提取”只保留对话中和当前任务强相关的事实。生产环境建议多种策略组合而不是只靠截断。5.6 循环控制与终止最后一步是循环控制。Harness 需要设定最大工具调用轮数防止 Agent 陷入死循环。常见的终止条件有三个模型返回了最终答案且没有工具调用达到最大轮数累计 token 超过安全阈值。建议把最大轮数设置得保守一些例如 5 到 8 轮并在日志中记录每一轮的上下文 token 数方便事后分析。6. 完整示例一个最小可运行的 Python LLM Harness现在我们把上面的流程落到代码里。这个示例包含三个文件配置文件、Token 工具类、Harness 主类外加一个调用入口。代码保持了最小可用性重点演示上下文组装、预算控制和工具调用循环。6.1 配置文件 config.yaml# config.yaml model: gpt-4o-mini # 根据实际可访问的模型修改 temperature: 0.3 max_tokens: 4096 context_budget: system: 0.15 fewshot: 0.10 history: 0.20 retrieval: 0.30 output: 0.15 reserved: 0.10 retrieval: top_k: 3 max_chunk_chars: 800context_budget是这份配置的核心它把总 token 预算按比例拆给了系统指令、示例、历史、检索和输出。max_tokens在这里被当作“上下文总预算”使用实际项目中它应该小于模型真实上限留出安全余量。6.2 Token 工具类 token_utils.py# token_utils.py 基于 tiktoken 的 Token 统计与截断工具。 import tiktoken class TokenCounter: def __init__(self, model: str): self.encoding tiktoken.encoding_for_model(model) def count(self, text: str) - int: return len(self.encoding.encode(text)) def truncate(self, text: str, limit: int, keep_head: bool True) - str: tokens self.encoding.encode(text) if len(tokens) limit: return text if keep_head: return self.encoding.decode(tokens[:limit]) return self.encoding.decode(tokens[-limit:])truncate方法提供两种截断方向keep_headTrue保留头部适合截断检索内容时保留开头信息keep_headFalse保留尾部适合截断历史消息时保留最近的对话。实际项目中截断策略要配合 token 计数一起验证避免出现半个 token 被拆开的情况。6.3 Harness 主类 harness.py# harness.py 一个最小可运行的 LLM Harness负责上下文组装、预算控制与工具调用循环。 import json from typing import Callable from openai import OpenAI import yaml from token_utils import TokenCounter class LLMHarness: def __init__(self, config_path: str config.yaml, tools: dict[str, Callable] | None None): with open(config_path, r, encodingutf-8) as f: cfg yaml.safe_load(f) self.cfg cfg self.model cfg[model] self.counter TokenCounter(self.model) self.client OpenAI() # 通过 OPENAI_API_KEY / OPENAI_BASE_URL 环境变量配置 self.tools tools or {} self.messages: list[dict] [] def budget_for(self, key: str) - int: return int(self.cfg[max_tokens] * self.cfg[context_budget][key]) def build_system(self, retrieval_text: str) - str: system 你是一名严谨的工程助手。请优先依据参考资料回答若资料不足请明确说明。 if retrieval_text: system f\n\n参考资料\n{retrieval_text} return system def add_message(self, role: str, content: str) - None: self.messages.append({role: role, content: content}) def trim_history(self) - None: 按预算截断历史消息只保留最近的消息。 history_limit self.budget_for(history) kept: list[dict] [] total 0 for m in reversed(self.messages): if m[role] system: kept.insert(0, m) continue cost self.counter.count(m[content]) if total cost history_limit: break kept.insert(0, m) total cost self.messages kept def _tool_schemas(self) - list[dict]: schemas [] for name, fn in self.tools.items(): schemas.append({ type: function, function: { name: name, description: fn.__doc__ or name, parameters: { type: object, properties: {}, additionalProperties: False, }, }, }) return schemas def _call_model(self): return self.client.chat.completions.create( modelself.model, messagesself.messages, toolsself._tool_schemas() or None, temperatureself.cfg[temperature], max_tokensself.budget_for(output), ) def run(self, user_input: str, retrieval_text: str ) - str: self.messages [{role: system, content: self.build_system(retrieval_text)}] self.add_message(user, user_input) for step in range(5): # 最大 5 轮工具调用防止死循环 self.trim_history() resp self._call_model() msg resp.choices[0].message if msg.tool_calls: self.messages.append(msg) for tc in msg.tool_calls: fn self.tools.get(tc.function.name) if fn is None: raise ValueError(f未知工具{tc.function.name}) result fn(**json.loads(tc.function.arguments)) self.messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse), }) continue self.messages.append(msg) return msg.content or return 已达到最大工具调用轮数请重试。这个类有几个值得注意的设计点。build_system把检索内容拼进了系统指令而不是单独作为一条消息这样可以让参考资料处在指令语言的约束范围内减少被模型忽略的概率。trim_history采用简单的倒序保留策略从最新的消息开始向前保留直到历史预算耗尽。_tool_schemas目前只生成最小参数结构生产环境需要根据工具真实签名生成更完整的 JSON Schema。6.4 调用入口 main.py# main.py from harness import LLMHarness def search_weather(city: str) - dict: 查询指定城市当前天气。 # 这里仅是示例实际请接入天气服务 return {city: city, weather: 晴, temperature: 26} def calc(expr: str) - dict: 计算简单四则运算。 # 注意示例用 eval 演示生产环境必须替换为受控表达式解析器 try: result eval(expr, {__builtins__: {}}, {}) return {expr: expr, result: result} except Exception as e: return {error: str(e)} if __name__ __main__: harness LLMHarness(config.yaml, tools{ search_weather: search_weather, calc: calc, }) # 模拟 RAG 召回结果 retrieval_text ( 根据内部知识库2026 年 Q1 的发布窗口是 3 月 15 日 发布前需要完成灰度验证。 ) answer harness.run( 帮我查一下杭州天气然后计算 25*48并总结发布窗口。, retrieval_textretrieval_text, ) print(answer)calc函数使用eval只是为了演示工具调用流程。这里必须郑重提醒在真实项目中永远不要直接对模型生成的参数执行eval否则等于给模型开了一个代码执行后门。正确做法是使用受限表达式解析库例如asteval或者把计算逻辑拆成白名单函数再或者把表达式交给专门的沙箱执行器。这属于安全红线不能因为图省事而妥协。7. 运行结果与效果验证先设置环境变量然后运行入口脚本export OPENAI_API_KEY你的 API Key export OPENAI_BASE_URL你的服务地址 python main.py如果链路正常模型应该先发现需要调用search_weather工具再调用calc工具最后结合检索内容给出总结。预期输出示意大致如下杭州当前天气为晴气温 26℃。 25*48 108。 根据内部知识库2026 年 Q1 的发布窗口是 3 月 15 日发布前需要完成灰度验证。怎么判断这次运行是成功的而不只是“有输出”建议从三个方面验证。第一检查工具调用是否真的发生了。在代码里给_call_model前后各加一条日志打印message.tool_calls的内容确认模型确实生成了工具调用而不是直接把天气信息幻觉出来。第二检查上下文预算是否生效。在run方法的循环里打印当前messages的总 token 数确认trim_history之后没有超过history预算。第三检查检索内容是否真正影响了回答。把retrieval_text改成明显冲突的信息例如发布窗口改成 4 月 1 日看模型是否以新资料为准。如果运行失败第一优先级是检查 API 连通性。看报错信息是连接超时、认证失败还是模型名不存在其次检查OPENAI_BASE_URL是否拼写正确很多第三方服务要求以/v1结尾最后检查 tiktoken 的模型映射是否失败如果你的模型名不是 OpenAI 官方系列encoding_for_model会抛异常这时可以把TokenCounter的初始化改为指定编码名称或者直接使用服务返回的 Token 数。8. 常见问题与排查思路结合日常开发中出现频率最高的问题整理成下面的排查表。建议收藏备用遇到问题按表格顺序检查。问题现象可能原因排查方式解决方案请求报上下文超限历史消息与工具结果持续追加没有做压缩打印每轮 messages 总 token 数启用 trim_history设置历史预算上限模型不调用工具工具 Schema 参数结构不完整查看模型返回的 content 是否在“假装调用”完善 parameters 的 properties 定义增加工具使用示例模型反复调用同一个工具工具结果格式不规范模型没看懂检查 roletool 消息的 tool_call_id 是否匹配统一结果 JSON 格式加入成功/失败状态字段检索内容被模型忽略检索内容放在了系统指令之前或与指令冲突检查最终请求消息顺序将参考材料拼接在系统指令内明确“优先依据资料”Token 统计和模型实际不一致tiktoken 模型映射不准对比 API 返回的 usage 字段改用服务商返回的 token 数或使用服务商 Tokenizer回答在中间被截断输出预算不足查看返回的 finish_reason 是否为 length提高 output 预算比例或启用流式输出多轮对话后效果急剧下降历史窗口只截断不压缩关键信息丢失检查历史保留策略对早期对话做摘要压缩再进入滚动窗口这里重点展开最容易忽视的一个问题工具结果的回填格式。很多新手把工具结果直接拼成字符串塞给模型没有保留tool_call_id或者把结果塞进了user角色消息。一旦消息结构和模型预期不一致模型就无法建立“这次调用对应那次指令”的关联随后就会陷入重复调用同一个工具的循环。所以只要涉及 Function Calling就必须严格遵循assistant消息带tool_calls、tool消息带tool_call_id的结构。这也是 Harness 之所以需要被认真设计的原因这些看似琐碎的细节正是上下文工程的核心战场。9. 最佳实践与工程建议9.1 把 Prompt 当作代码管理系统指令、少样本示例、RAG 模板凡是会进入上下文的文本都应该纳入版本管理。不要直接修改线上 Prompt而是像改代码一样走评审流程。推荐的目录结构是prompts/system/v1.md、prompts/fewshot/xxx.yaml每个版本都记录变更原因。这样当线上效果波动时你能快速定位是哪一次 Prompt 变更引发的。9.2 给上下文拍快照排查 LLM 应用问题最痛苦的地方在于“不可复现”。建议在 Harness 的每次请求里打印一份上下文快照完整的 messages 结构、每部分的 token 数、工具调用顺序、最终输出。可以把快照写入本地日志或追踪系统线上出现问题时直接按 request_id 回放上下文。没有快照你只能靠猜。9.3 预算控制要留安全余量不要把max_tokens直接设成模型的真实上限。因为工具结果回填、模型输出、TikToken 统计误差都会额外消耗 Token。安全做法是把总预算设为模型上限的 70% 到 80%并在保留空间里留出至少 10% 给不可预知开销。宁可少塞一点资料也不要让请求在最后一刻被系统强制截断。9.4 工具权限做最小化设计Harness 暴露给模型的工具本质上是模型可以触达的执行能力。每增加一个工具就增加一分被误调用的风险。上线前应该问三个问题这个工具是否必须由模型调用参数是否经过了 Schema 约束执行结果是否会包含敏感信息涉及删除、写入、支付等高风险操作一定要增加人工确认环节或者要求调用方提供额外授权凭证。9.5 关注上下文工程的新方向上下文工程并不是一个静态领域。从社区最近的讨论来看几个方向值得持续关注。一个是 Karpathy 提出的 LLM Wiki 范式强调把上下文沉淀成可持续维护的结构化知识库而不是每次请求都临时拼装另一个是 Agentic Skill Evolution让智能体在运行中不断沉淀可复用的技能实现“元上下文工程”。这些方向本质上都在做同一件事让上下文从一次性消耗品变成可积累、可复用、可进化的资产。9.6 下一步行动建议这篇文章讲清楚了三件事第一Context Engineering 是 LLM 应用质量的分水岭Harness 是它的工程载体第二上下文管理的关键在于预算分配、顺序设计、工具回填规范和循环终止条件第三最小可用的 Harness 并不复杂难点在于把它做成可观测、可控制、可演进的生产系统。建议你今天就用文中的最小示例跑通一个完整闭环然后做两件小改进给 Harness 增加上下文快照日志再把你的 RAG 资料接入build_system观察检索内容对回答质量的实际影响。跑通之后你会开始用“上下文工程”的视角去看每一个 LLM 应用很多之前说不清的效果波动都会在这个视角下变得清晰起来。
返回列表