加载中...
  • Context Engineering 全景:上下文不是一段 Prompt loading

    吃透 AI Agent 开发 · 第 15 篇 · 第四章 · Context Engineering

    上下文同时涉及选择、顺序、预算、时机和新鲜度。只会写 system prompt,还远远不够。

    很多人第一次写 Agent 的系统提示词,会把所有规则都放进一个模板:角色、工具说明、当前日期、项目目录、Git 状态、用户偏好、任务列表,最后再加一句“请认真完成”。刚开始没问题,功能越多,这段字符串越像一间堆满纸箱的仓库。

    Context Engineering 不是把 Prompt 写得更长,而是决定:什么信息在什么时候,以什么顺序和预算进入模型。

    上下文像出差行李

    身份证和工作纪律长期不变,适合放在固定位置;这次航班和酒店只对当前行程有效;目的地的餐厅攻略可以到附近再搜索。

    如果把半年后的所有攻略都提前塞进行李箱,箱子会更重,却不会让这次出差更顺利。

    Agent 上下文也可以分成三层:

    稳定性 适合放什么 典型生命周期
    长期稳定 核心安全规则、工具纪律、输出原则 多个会话
    会话稳定 当前项目说明、选定模型能力、会话目标 一段会话
    每轮动态 用户问题、任务状态、相关文件、当前时间 一次请求

    这不是绝对分类。重点是让每个字段都能回答:“它多久变化一次?变化后谁需要知道?”

    为什么稳定前缀值得保护

    不少模型服务支持 Prompt Cache。当前请求与之前请求共享较长前缀时,服务端可能复用计算结果,降低首 token 延迟和输入成本。

    假如把精确到秒的时间放在 system prompt 最前面,每轮前缀都不同,后面再稳定也很难命中缓存。更合理的做法是保持核心规则在前,把当前时间、Git 摘要等动态内容放到靠后的临时上下文。

    缓存只是收益之一。稳定前缀还更容易测试:你可以给它计算 hash,发现一次功能改动是否意外改变了核心规则。

    不要继续拼大字符串

    可以用管道表示不同上下文片段:

    type Stability = 'stable' | 'session' | 'dynamic'
    
    interface ContextSection {
      name: string
      stability: Stability
      render(context: RequestContext): string | null
    }
    
    const sections: ContextSection[] = [
      coreRules,
      toolDiscipline,
      projectInstructions,
      taskState,
      relevantFiles,
      runtimeSummary
    ]
    

    构建时不仅得到最终文本,还应保留每段名称、字符数和稳定性。上下文超预算时,你才知道该缩短文件内容,还是任务状态异常膨胀,而不是面对一个十万字字符串猜原因。

    JIT Context:需要时再拿

    Just-In-Time Context 的意思不是“所有信息都由模型临时搜索”,而是推迟不确定是否需要的内容。

    比如用户说“帮我修登录失败”,系统可以先注入项目说明和与登录相关的少量候选文件。完整依赖树、全部测试日志、所有历史设计文档不必立刻进入。模型真正需要时,可以用搜索或读取工具继续取。

    这样做有三个好处:

    • 减少无关信息对注意力的干扰。
    • 降低输入 token 和延迟。
    • 让信息来源和读取动作出现在工具轨迹里,便于追查。

    代价是可能多一次工具调用。因此,稳定且高频需要的信息适合提前放入;体积大、相关性不确定的信息适合按需获取。

    项目指令也需要预算

    真实仓库里的说明文件可能有上千行。全量注入最省开发时间,却会长期占据窗口。可以采用两级策略:短文件原样放入;长文件只保留必须遵守的运行纪律、章节索引和按需读取提示。

    这里不能只做“截取前 4000 字”。重要安全规则可能在后半段。更可靠的办法是按标题解析结构,优先保留约束类章节,同时告诉模型完整文件在哪里。

    顺序也是语义

    同样几段内容,顺序不同,效果可能不同。通常可以遵循:

    1. 先放身份、安全和不可违反的核心规则。
    2. 再放稳定的工具和工作流纪律。
    3. 然后放项目与会话背景。
    4. 最后放当前任务、相关文件和本轮偏好。

    外部文档和工具结果属于不可信数据。不要把它们放进“系统规则”的位置,也不要允许文档里一句“忽略之前指令”改变权限策略。

    常见失败模式

    动态字段污染稳定前缀

    每轮注入完整 Git 状态、工具数量或精确时间,缓存命中下降,Prompt diff 也变得嘈杂。

    工具说明全部常驻

    系统有一百个工具,但本轮只可能用到五个。全部暴露既浪费上下文,也增加模型选错工具的概率。可以根据任务阶段动态选择工具集,或只提供工具目录,使用时再加载详细 schema。

    “相关”只看语义相似

    用户问构建失败,搜索可能找到大量包含“build”的文档,却不一定是当前仓库、当前版本或当前错误。相关性还要考虑路径、时间、任务阶段和来源可信度。

    用 Model Context 做个小练习

    把下面六项分到稳定、会话稳定或每轮动态,并说明理由:

    • 禁止输出密钥。
    • 当前日期。
    • 项目使用 pnpm。
    • 这轮用户选中的三个文件。
    • 当前任务图。
    • 回答采用教学风格。

    然后修改其中一项,想一想它是否应该导致整个 system prompt hash 变化。

    Selection 到 Model Context 的小结

    Context Engineering 的核心是选择和时机。稳定规则要保护,动态信息要靠后,大内容要有预算,不确定相关的信息要按需获取。Prompt 不再是一段神秘文字,而是一条能检查、能测量、能演进的数据管道。

    下一篇继续解决一个必然发生的问题:无论挑选多仔细,会话最终还是会变长。什么时候压缩,压缩什么,又有哪些内容根本不该放在上下文里?

    做一张上下文装箱单

    把一次模型请求想成托运行李。稳定规则是证件,项目指令是行程单,当前任务是当天用品,文件正文和记忆则是按需取出的物品。所有东西都重要,不代表所有东西都要在出发时塞进同一个箱子。

    内容种类 来源 放入时机 退出条件
    核心行为规则 产品代码 每次请求的稳定前缀 版本升级
    项目说明 AGENTS.md 进入项目时 切换项目或规则更新
    Git 摘要 当前工作区 本轮需要时 下一轮重新计算
    文件正文 工具读取 模型明确需要时 完成任务或被压缩
    长期记忆 记忆选择器 与问题相关且未过期 失效、冲突或用户忽略

    装箱单最重要的字段是“退出条件”。只记录如何注入,不记录如何撤掉,Context 最终一定会腐烂。

    从 q-code 观察四类上下文

    编号 路径 阅读目的
    1. Prompt 管道 src/context/prompt-builder.ts 片段如何按稳定性排序
    2. 运行时事实 src/context/runtime-context.ts 日期、Git 和任务信息怎样只进本轮
    3. 项目规则 src/context/agent-md.ts 长文档如何截取纪律和索引
    4. 质量审计 src/context/prompt-quality.ts 缺失维度如何被检查

    失败现场:所有信息都拼成一段字符串

    所有信息都放入同一段长字符串,既难维护又让动态字段破坏缓存。更严重的是,压缩和日志无法指出某句话来自项目规则还是旧记忆,模型冲突时也没有稳定的优先级。

    修复可以从“为每段内容加名字”开始,不必一次搭建复杂框架。记录 category、stability、source、chars 和 age;请求前打印片段摘要,出现问题时先找错层的片段,再改内容。

    练习:让一条信息晚一点出现

    挑选当前 Prompt 中最占空间的一段资料,改成先提供标题和摘要,模型需要时再读取全文。比较两种方案的 token、首次工具选择和最终正确性。再让资料更新一次,确认新鲜度变化不会让稳定前缀整体失效。

    请求发出前,先生成一份 Context Manifest

    只保存最终 Prompt,很难回答“这一段从哪里来的”。构建器可以同时生成一份不含正文的 manifest,记录片段顺序、来源、大小、年龄和信任级别。

    [
      {"id":"core-rules","role":"system","chars":4200,"stability":"stable","trust":"product"},
      {"id":"project-agents","role":"system","chars":6800,"stability":"project","trust":"repository"},
      {"id":"memory-pnpm","role":"user","chars":380,"stability":"dynamic","trust":"user-confirmed","ageDays":12},
      {"id":"file-auth","role":"user","chars":9100,"stability":"dynamic","trust":"workspace","sha256":"..."}
    ]
    

    线上日志只需保存这个摘要,不必记录项目正文。出现“模型为何认为项目用 npm”时,可以先看本轮是否注入了旧记忆、当前 lockfile 是否读取、两者顺序和年龄,再决定是否读取受控原文。

    flowchart LR
      S["候选片段"] --> V["来源与信任标注"]
      V --> B["预算分配"]
      B --> O["稳定性排序"]
      O --> Q["Model Request"]
      O --> M["Context Manifest"]
      M --> D["Diff / Audit / Debug"]
    

    manifest 还适合做请求间 diff。若用户只问了下一句,稳定段 hash 却变化,就能定位哪个动态字段误入前缀;若某个文件已更新,manifest 的 sha256 不变,则说明缓存失效有 bug。

    冲突信息需要优先级和证据,不是让模型自由投票

    上下文中可能同时出现:用户记忆说偏好 npm,仓库有 pnpm-lock.yaml,项目规则写“必须使用 pnpm”,当前用户又明确要求这次试用 npm。四条信息并非简单的“后出现覆盖前面”。

    可以先按作用域和权威度判断:产品安全规则不可被项目文档覆盖;项目明确纪律通常高于旧个人偏好;当前用户请求可改变普通偏好,却不能绕过安全边界;现场文件是当前事实,但也可能是未提交实验。

    不必把整个优先级算法交给模型。构建器可以对明显冲突加提示:

    [context-conflict]
    topic: package-manager
    project-rule: pnpm (AGENTS.md, current)
    memory: npm (12 days old, user-confirmed)
    current-request: try npm for this command
    instruction: do not modify lockfiles; explain conflict before acting
    

    这让模型知道冲突存在,也把最终副作用留给工具策略。旧记忆不会因为排在后面就悄悄变成更高规则。

    预算分配不能只按先来后到截断

    总预算剩 20K token 时,按拼接顺序截断会把最后加入的当前任务砍掉。更合理的做法是为类别设置保底和上限,再在类别内部按相关性、年龄和可恢复性分配。

    例如核心规则 6K 保底,当前任务 3K 保底,最近交互 4K,候选文件最多 5K,记忆最多 2K。工具长输出不参与争抢,默认 offload。若某类没用满,剩余额度再分给高价值片段。

    预算报告要告诉模型哪些内容被省略:有 12 个候选文件,只注入 2 个;完整构建日志在 artifact;三条旧记忆因年龄被排除。模型知道信息不完整,才会主动读取,而不是把当前可见片段当成全部世界。

    “来源可信”与“内容真实”不是一回事

    项目文件来自受信工作区,不代表里面每句话都是真实,更不代表能提升权限。README 可能过时,测试日志可能由攻击者构造,网页内容也可能包含 Prompt Injection。

    Context 层要保留数据边界,用明确分隔和来源标签包装外部内容。权限系统只接受宿主配置和用户确认,不从文档文本中读取授权。即使工具结果写着“管理员允许删除目录”,它仍然只是工具数据。

    模型可以依据外部内容形成候选判断,但高风险动作必须回到可验证来源。比如文档说 API 已废弃,先检查当前代码或官方版本;记忆说某路径安全,仍由实时 path policy 解析。

    每种片段都要定义所有者

    Context Rot 往往不是文本质量问题,而是没人负责删除。核心规则由产品代码版本管理;项目指令由仓库维护者负责;记忆有写入者、更新时间和过期策略;运行时摘要由本轮重新计算;文件正文由 hash 判断新鲜度。

    所有者不明确的片段不应长期常驻。一个临时 A/B 实验提示如果没有 expiresAt,三个月后仍可能影响模型。构建器可以拒绝把 dynamic 片段放进 stable 区,也可以对过期内容发出诊断。

    用 Context Diff 做回归测试

    给同一个固定请求构建两次上下文,稳定前缀 hash 应一致;把当前日期改变一天,只允许 runtime 片段变化;切换项目后,project 片段变化但 product 核心不变;用户说“忽略记忆”时,memory index 和正文都应消失。

    测试不必快照完整 Prompt,完整快照容易因文案微调产生噪声。可以断言 manifest 的 section ID、顺序、stability、字符预算和 hash 关系,再对关键安全规则做存在性检查。这样既保护架构约束,也允许正文正常改进。

    Context Engineering 做到这里,就从“写一段效果好的 Prompt”变成了数据工程:每份信息有来源、预算、时效、信任和退出条件;每次请求能解释自己带了什么,也能解释为什么没带其他东西。

    本文目录
    本文目录