Context Engineering 实战 03|运行时该给模型看什么:动态上下文组装
AIReview 的 code review 系统有 200 条 review 规则,早期全量塞进 context。上线第一周就发现问题:模型给一个纯前端的 React 组件提了"SQL 查询应该加索引"的 review 意见。
排查后发现不是个例。200 条规则全量注入时,模型的行为分成三类:
模型行为 比例 说明
──────── ──── ─────────
正确遵循相关规则 12% 审到点子上了
强行套用不相关规则 16% 给前端组件查 SQL 注入
忽略规则 72% 大部分规则直接忽视
只有 12% 的规则被正确使用。更多的规则被忽略或乱用。
改成动态选择后——按文件类型 + diff 内容选 10-15 条相关规则注入——遵循率从 45% 到 83%,误报(强行套用不相关规则)从 22% 降到 4%。
这个变化不是 prompt 优化——是把"拼字符串"变成了"pipeline 工程"。
五阶段 Pipeline
把上下文组装拆成五步,每步做一件事:
Source → Filter → Rank → Trim → Assemble
│ │ │ │ │
│ │ │ │ └→ 按注意力曲线排列位置
│ │ │ └→ 按 token 预算裁剪
│ │ └→ 按语义相关度排序
│ └→ 按硬规则过滤不相关的
└→ 收集所有候选 context
| 阶段 | 输入 | 操作 | 输出 | 可观测指标 |
|---|---|---|---|---|
| Source | 用户请求 | 收集所有可用的 context 来源 | 候选集合 | 来源数、总 token |
| Filter | 候选集合 | 按类型/语言/条件过滤 | 过滤后的集合 | 过滤比例 |
| Rank | 过滤后的集合 | embedding 语义排序 | 排序后的 top-k | 最低相关度 |
| Trim | 排序后的列表 | 按预算裁剪低优先级项 | 预算内的 context | 截断条数 |
| Assemble | 预算内 context | 按 U 形注意力排列 | 最终 prompt | 各部分占比 |
每个阶段可以独立优化、独立加日志、独立出 bug。这是"工程化"的核心——拆到每一步都可以单独诊断。
Source:先把所有牌亮出来
一次 AIReview 的 code review 请求,所有可能对判断有帮助的信息:
来源 内容 预估 token
────── ────── ────────
diff 本次代码变更 200-2000
file_context 被修改文件的完整内容 500-5000
related_files import 链上下游文件 500-3000
review_rules 200 条 review 规则 6000
pr_description PR 描述和 commit message 100-500
history_comments 这个文件的历史 review 意见 200-1000
author_patterns 该作者的历史踩坑模式 100-500
总计可能 8000-18000 token。而 context window 里给动态内容的预算只有 4000-6000 token(system prompt、工具定义、对话历史已经占掉了一大块)。
这一步不做任何过滤——先把所有可能有用的信息源列出来。
Filter:硬规则砍掉明确不相关的
Filter 用确定性规则做粗筛。不做语义理解——只做条件匹配。
def filter_rules(all_rules, diff_info):
filtered = []
for rule in all_rules:
if rule.file_types and diff_info.file_type not in rule.file_types:
continue
if rule.languages and diff_info.language not in rule.languages:
continue
if rule.change_types and diff_info.change_type not in rule.change_types:
continue
filtered.append(rule)
return filtered
一个 .tsx 文件的 diff 进来,SQL 规则、Python 规则、Go 规则全部砍掉。200 条 → 30-50 条。
Filter 的原则:宁可多留,不要误删。 这一步砍掉的应该是"明确不相关"的——给 React 组件检查 SQL 注入显然不相关。但"函数不要超过 50 行"这种通用规则,留着,交给下一步判断。
Filter 出过一次 bug:一条 “API 返回格式统一” 的规则被标记为 languages: ["python"],但实际上前后端都需要这条规则。Filter 把它在 TypeScript 的 review 中过滤掉了,导致几个 API 返回格式不一致的问题漏检。
修复:给这条规则的 languages 字段改成空(不限语言),让它进入 Rank 阶段由语义匹配来决定。Filter 的规则要尽量宽松——宁可多进一些到 Rank,不要在这里误杀。
Rank:语义匹配选最相关的
Filter 之后的 30-50 条都是"可能相关"的。Rank 用 embedding 相似度找出"最相关"的。
def rank_rules(filtered_rules, diff_text, top_k=15):
diff_emb = embed(diff_text)
scored = []
for rule in filtered_rules:
score = cosine_similarity(diff_emb, rule.embedding)
scored.append((rule, score))
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:top_k]
工程细节:
规则 embedding 预计算。 200 条规则的 embedding 在启动时算好存缓存。运行时只需要算一次 diff 的 embedding + 200 次余弦相似度计算。延迟在 50ms 以内。
top_k 不是固定的。 设一个相关度下限(比如 0.65)。如果 top-15 里只有 8 条超过 0.65,就只注入 8 条。宁缺毋滥——一条不相关的规则比少一条相关的规则更有害。
AIReview 实际运行中发现的一个问题:Rank 阶段选出的 top-15 里偶尔会有两条互相矛盾的规则。比如一条说"错误处理用 try-catch",另一条说"这个模块用 Result 模式不用 try-catch"。两条的 embedding 跟 diff 都相关(因为 diff 里确实有错误处理代码),但同时注入会让模型困惑。
解决方法:在 Rank 之后加一步去重——如果两条规则的 embedding 相互相似度大于 0.9,只保留跟 diff 相关度更高的那条。
Trim:预算裁剪
Rank 之后可能还超预算。Trim 不是从后面简单截断——而是按优先级压缩。
优先级 规则类型 处理方式
────── ────── ─────────
P0 安全类 保留完整描述,不压缩
P1 Bug 类 保留完整描述
P2 性能类 压缩到一句话摘要
P3 风格类 超预算时第一批被砍
一条 P2 规则从"当函数参数超过 5 个时,应该考虑将参数封装为对象,这样可以提高可读性和可维护性,并且方便添加默认值和验证逻辑"(48 token)压缩成"参数 > 5 个时封装为对象"(12 token)。
Trim 的预算不要用满。留 10-15% 的余量——context 完全塞满的模型输出质量会下降,需要给模型"思考空间"。
Assemble:位置排列
最后把选好的信息按正确的位置拼装。Transformer 的注意力分布是 U 形——开头和结尾高、中间低。
AIReview 最终的拼装顺序和每块的实际 token 预算:
位置 内容 token 原因
──── ────── ───── ────
开头 Base Layer(身份 + 输出格式) 300 注意力最高区
紧跟开头 动态 review 规则(top-k) 400 重要规则在高注意力区
中段上部 相关文件上下文 800 辅助信息,注意力低一点也行
中段下部 PR 描述 + 历史 review 意见 300 背景信息
紧贴用户输入 diff 内容 500 模型处理时会重点关注
结尾 Guard Layer(安全约束) 50 U 形右端高注意力区
─────
总计 2350
跟全量注入的对比:
方式 总 token 规则遵循率 误报率
────── ──────── ───────── ─────
全量注入 ~12000 45% 22%
Pipeline 动态选择 ~2350 83% 4%
token 减少 80%,遵循率翻倍,误报率降到五分之一。
Pipeline 的可观测性:出问题时 5 分钟定位
每个阶段加一行日志。不用复杂的监控系统——在每个阶段输出关键数字就行:
[Source] sources=7, total_tokens≈14200
[Filter] rules: 200→38, file_type=typescript, language=ts
[Rank] top_k=15, min_score=0.71, max_score=0.94
[Trim] budget=500, actual=480, trimmed=0
[Assemble] total=2350, rules=17%, diff=21%, file_ctx=34%
一次真实的排查过程:模型在 review 一个 Python 文件时漏掉了明显的类型错误。看日志:
[Filter] rules: 200→42, file_type=python
[Rank] top_k=15, min_score=0.58, max_score=0.87
min_score 只有 0.58——低于通常的 0.65 下限。说明 Rank 阶段选出的 top-15 里有相关度很低的规则混入。追查发现:Python 类型相关的规则描述用的是"type hints"和"annotation",但 diff 里用的是"typing"这个关键词。embedding 空间里"typing"和"type hints"的相似度只有 0.62。
修复:给 Python 类型规则的描述里补上"typing"关键词,重新算 embedding。修复后 min_score 回到 0.72。
没有 pipeline 日志,这个问题的排查路径是:看最终输出 → 猜是哪个规则漏了 → 猜是模型没遵循还是规则没注入 → 手动检查 context。几个小时。有了日志,从 Rank 的 min_score 异常直接定位,20 分钟。
三个常见误区
误区 1:全塞。 "为了不遗漏,所有信息都给模型看。"结果:45% 遵循率。
误区 2:随机选。 "随机抽 15 条应该差不多?"实测:随机 15 条的遵循率 52%——比全塞好一些(token 少了),但远不如语义匹配的 83%。随机选中的规则可能跟当前 diff 无关。
误区 3:只按长度截断。 "超预算了从后面砍。"后面的可能恰好是最重要的。应该先 Rank 再 Trim——排序在前,截断在后。
方式 选中规则数 遵循率 误报率
────── ───────── ────── ─────
全塞 200 条 200 45% 22%
随机选 15 条 15 52% 14%
语义匹配 top-15 15 83% 4%
好的 Context Engineering 不是写得多,是选得准。把上下文组装当 data pipeline 来设计,每个阶段都可以独立优化和测试。

被折叠的 条评论
为什么被折叠?



