Agent 不是知道得越多越好
假设你请一位新同事修复支付 Bug,却一次性把公司五年的聊天记录、所有代码仓库、完整监控日志和几十份旧规范都塞给他。他获得的信息很多,但真正有用的线索反而更难找到。
Agent 也一样。
上下文窗口再大,也不等于应该把所有内容放进去。模型做判断时真正需要的是:与当前任务相关、来源清楚、足够新鲜、优先级明确,而且长度可控的信息。
负责选择、组织和维护这些信息的系统,就是 Harness 的第二个核心部分:上下文引擎。
如果把模型比作厨师,上下文引擎不是仓库,而是备菜台。仓库里可以有一万种食材,但当前这道菜只应该摆上真正要用的那几样。
本文是《把 Harness 讲清楚:Agent 背后的运行系统》八个核心部分的第二篇。
什么是上下文
上下文是一次模型调用时,模型实际能够看到的全部信息。它通常包含:
- 系统规则和身份说明;
- 当前用户请求;
- 最近的对话历史;
- 任务契约和当前计划;
- 项目规范与相关文件;
- 工具定义和工具结果;
- 长期记忆或用户偏好;
- 当前环境、时间和运行状态。
上下文并不是数据库中“已经保存”的全部内容。只有被选中并组装进本次请求的内容,才会影响模型判断。
所以,上下文工程的核心问题不是“我们拥有什么数据”,而是“在这一刻,模型应该看到什么”。
上下文引擎的五项工作
发现
根据任务找到候选信息,例如相关文件、目录内规则、历史决策、接口文档和最近的工具结果。
筛选
移除与当前任务无关、重复、过期或可信度不足的内容。
排序
让不可违背的系统规则优先于项目建议,让当前任务信息优先于陈旧历史,让直接证据优先于模型推断。
压缩
当任务变长时,把已经完成的探索过程压缩成事实、决策和未解决问题,而不是永久保留全部原始输出。
标注来源
明确一段信息来自用户、项目文件、工具结果、外部网页,还是 Agent 自己的推断。来源不同,可信度和可执行权限也不同。
一次上下文组装的流程
任务契约 + 当前步骤
↓
生成信息需求
↓
检索文件、记忆、规则和工具结果
↓
去重、过滤、检查新鲜度和权限
↓
按优先级与 token 预算排序
↓
必要时摘要或截取
↓
组装本次模型请求
↓
记录“模型实际看到了什么”
最后一步很重要。没有可追踪的上下文快照,出现错误时就无法判断到底是模型推理错了,还是 Harness 给了错误资料。
上下文不是越多越好的三个原因
噪声会稀释重点
当真正关键的三条规则埋在几万行日志中,模型更容易忽略它们。长上下文解决了容量问题,却没有自动解决注意力分配问题。
过期信息会制造冲突
旧文档说接口字段叫 userId,新代码已经改为 accountId。如果两份内容同时出现却没有时间和来源标记,模型很可能选择错误版本。
上下文有真实成本
更多 token 意味着更高延迟和费用。频繁变化的长前缀还会降低缓存复用。上下文引擎需要把信息价值当作有限预算来分配。
给信息建立优先级
可以为候选内容计算一个简单分数:
score = 相关性 × 新鲜度 × 可信度 × 必要性 - token 成本
这不是必须采用的数学公式,而是一种思维方式。
- 相关性:是否直接帮助当前步骤;
- 新鲜度:是否仍然有效;
- 可信度:来自源码、工具事实,还是未经验证的推断;
- 必要性:不提供它是否会违反规则或导致错误;
- 成本:加入后占用多少上下文空间。
系统规则和安全边界不应只靠分数竞争,它们属于必须保留项。评分更适合在大量候选资料之间做选择。
最小上下文选择器
下面用伪代码表达一个基础实现:
def build_context(task, step, budget):
required = load_required_rules(task)
candidates = []
candidates += search_workspace(step.query)
candidates += search_memory(task.objective)
candidates += recent_tool_results(task.id)
candidates = remove_duplicates(candidates)
candidates = reject_expired_or_unauthorized(candidates)
candidates = rank_by_relevance(candidates, step)
selected = list(required)
remaining = budget - token_count(required)
for item in candidates:
rendered = compress_if_needed(item, remaining)
if token_count(rendered) <= remaining:
selected.append(rendered)
remaining -= token_count(rendered)
return attach_source_metadata(selected)
真实系统还需要处理取消、检索失败、敏感信息脱敏和缓存,但这段代码已经表达了核心:先放必须信息,再用剩余预算选择最有价值的内容。
长任务为什么需要压缩
Agent 连续工作几十轮后,历史里会积累大量搜索结果、失败尝试和重复讨论。如果把它们原样带入每一轮,请求会越来越慢,也越来越容易跑偏。
压缩的目标不是简单缩短文本,而是保留未来决策仍然需要的状态:
原始历史
├─ 尝试过哪些方案
├─ 每次工具的完整输出
├─ 大量中间讨论
└─ 已经解决的问题
↓ 压缩
任务状态摘要
├─ 已确认事实
├─ 已采用决策及原因
├─ 已修改对象
├─ 尚未解决的问题
├─ 不得重复的失败方案
└─ 下一步与完成标准
好的摘要应当可以让一个没有看过前文的 Agent 接手,而不会重复破坏性操作或遗失关键约束。
记忆与上下文不是同一个东西
记忆是可能在未来被再次使用的信息存储,上下文是本轮真正提供给模型的内容。
例如,系统记住用户偏好使用中文,这是记忆;当前请求组装时把“使用中文回答”加入系统提示词,才成为上下文。
两者之间应该有检索和准入层。不能因为某条信息曾经被保存,就让它永久出现在每一次请求中。长期记忆还应支持来源、过期时间、用户更正和删除。
工具结果怎样进入上下文
工具返回内容往往很大。上下文引擎不应该把数据库全部行、完整构建日志或整份网页无条件回灌给模型。
更稳妥的做法是:
- 工具保留完整规范结果,便于程序处理和审计;
- 为模型生成有长度上限的视图;
- 明确标注是否截断,以及完整数据的位置;
- 只在模型确实需要时继续读取某个片段。
例如搜索工具可以返回总命中数、按文件分组的前 20 条结果和 truncated: true,而不是假装展示的是完整结果。
必须区分事实、规则和推断
把不同来源混在一段自然语言里,是上下文系统常见的危险做法。
更清楚的结构是:
[SYSTEM RULE]
生产环境禁止自动部署。
[USER REQUEST]
修复登录错误并准备发布说明。
[TOOL FACT]
测试 auth/session.test.ts 已通过,退出码为 0。
[AGENT INFERENCE]
根因可能是刷新令牌并发更新,需要继续验证。
工具输出也不应天然被当作指令。网页、邮件和仓库文件中都可能出现“忽略之前规则”之类的文本,它们是待分析数据,不是可以覆盖系统规则的新命令。
常见的失败方式
一次性读取整个仓库
看起来信息充分,实际浪费预算,并让关键文件失去突出度。应该先搜索和建立地图,再按需深入。
只做语义相似度检索
语义相似不等于权威。旧设计稿可能比当前源码更像用户问题,却已经失效。排序必须结合时间、来源和文件层级。
摘要时丢掉否定条件
“允许修改代码,但禁止更改数据库”如果被压缩成“允许修改项目”,风险会被放大。安全约束和未完成事项不应被模糊摘要。
不记录截断
模型看到前 100 行却以为看到了完整文件,会做出错误结论。所有截断视图都应该显式说明范围。
把模型推断写成永久事实
未经工具或用户验证的猜测,应该保持推断状态。只有获得证据后才能升级为事实。
从原型开始怎么做
可以先建立四层上下文:
- 固定系统与安全规则;
- 当前任务契约;
- 最近对话和工具结果;
- 按当前步骤检索出的项目资料。
然后为每一层设置明确预算,记录来源与截断信息。任务超过一定轮次时,把旧历史压缩成结构化状态摘要,并保留原始日志供审计,而不是继续塞给模型。
上下文引擎检查清单
- 模型本轮真正需要解决什么问题?
- 必须始终保留的规则有哪些?
- 候选资料是否检查了来源、新鲜度和权限?
- 是否去除了重复与无关内容?
- 工具结果过大时是否提供渐进式读取?
- 截断和摘要是否明确标记?
- 是否区分用户输入、系统规则、工具事实和 Agent 推断?
- 长任务摘要是否保留关键决策、未完成事项和禁止条件?
- 能否还原某次模型调用实际看到的上下文?
最后理解上下文引擎
模型能力决定了它“能不能想明白”,上下文引擎决定了它“拿什么来想”。
很多看似模型不够聪明的问题,实际是系统给了错误、过期或过量的信息。一个成熟 Harness 不追求把上下文窗口塞满,而是像一位可靠的研究助理:在正确的时间,把正确的证据摆到桌面上,并清楚说明每条信息从哪里来。
一句话总结:上下文的质量,通常比上下文的数量更重要。