跳到主要内容

上下文窗口是一个 API 界面:像对待合约一样对待你的提示词结构

· 阅读需 11 分钟
Tian Pan
Software Engineer

在一个生产环境中的 LLM 功能上线半年后,一名工程师提交了一个 bug:模型在上个季度的某个时间点开始给出错误的输出。没人记得改过提示词(Prompt)。Git blame 显示它为了“提高可读性”被清理过。之前的版本已经找不到了。调试工作只能从零开始。

就在这一刻,团队才发现他们的上下文窗口(context window)从未被真正工程化过——它只是被拼凑出来的。

上下文窗口是你的系统与模型之间的契约。进入其中的每一个标记(token)——系统指令、检索到的文档、对话历史、工具架构、用户查询——都是对一个函数调用的输入,这个调用既费钱又耗时,且会产生非确定性的输出。然而,大多数团队将上下文组合视为实现细节,而非 API 表面。提示词被就地编辑,没有版本控制。各部分通过累加增长。没有人负责布局。变化在无声无息中传播。调试体验比 LLM 时代之前的任何东西都要糟糕,因为至少堆栈跟踪(stack traces)会告诉你什么是改变了的。

问题:上下文窗口布局是“狂野西部”

在传统服务中,接口契约是显式的:输入有类型,输出有模式(schema),错误有代码。接口的变更会触发评审。破坏性的改动会立即暴露。

基于提示词的系统默认没有任何这种纪律。上下文窗口就是一块文本,在运行时通过字符串拼接、f-strings 或模板库构建而成。“部分”(Sections)——即使存在的话——也只是某人曾经理解过的散文式约定。没有 schema。没有版本。没有 diff。

失败模式是完全可以预见的:

  • 无声的性能偏移。 检索块(chunk)格式发生了变化,导致准确率下降了 8%,但没有“语义质量退化”的警报。回归测试就这样发布了。
  • 无法归因的破坏。 用户报告了一个回归问题。你今天可以复现它,但两周前的版本却不行,因为提示词在那之后已经改了三次。
  • 知识流失。 某条约束的原始作者离职了。没人知道系统提示词(system prompt)中第三段内容的含义。删掉它似乎很安全。事实并非如此。
  • 凭感觉调试。 评估一个提示词变更意味着手动运行几十次,形成一种直觉,然后合并。你不是在测试;你是在猜。

超过 65% 的 LLM 开发者表示,提示词版本控制和可观测性是将原型扩展到生产环境中最艰巨的挑战。差距不在于模型质量——而在于围绕上下文组合的工程纪律。

观念转变:将提示词视为 API 契约

像对待公共 API 一样对待你的提示词。契约应包含:

  • 目的(Purpose):该提示词产生的具体行为。
  • 输入(Inputs):带有约束的具名、强类型参数(例如:retrieved_chunks: string[],最多 5 项)。
  • 输出(Outputs):预期的响应 schema —— 格式、字段、错误表达。
  • 不变式(Invariants):在每次调用中必须保持的规则(例如:“不得泄露内部工具名称”)。
  • 版本(Version):语义版本或内容哈希,以便在出现 bug 时,你可以指向当时生效的确切契约。

在软件工程中,调用一个不知道契约的函数被认为是草率的。提示词工程常规性地在做类似的事情——然后疑惑为什么调试过程如此痛苦。

概念上的转变至关重要,因为它改变了你对待一等公民制品(first-class artifact)的态度。组装上下文窗口的代码不是简单的管道——它是 API 的实现。修改系统提示词中三个词的提交不是“小清理”——它是接口变更,在合并之前可能需要经过评估。

基于槽位的上下文架构

这种构思的实践表达是实践者所称的“基于槽位的上下文架构”:将上下文窗口视为一组具有明确职责的、具名的有限区域。

一个典型的上下文窗口大致有五个槽位:

  1. 系统槽(System slot):角色设定、任务定义、不变式、输出格式要求。这是稳定的部分——它应该很少更改,且更改必须经过评审。
  2. 上下文槽(Context slot):检索到的文档、背景知识、RAG 块。这是在运行时注入的,随每个请求而变化。
  3. 工具槽(Tool slot):可用工具的定义和 schema。使用动态工具注入时,此槽位的内容也是特定于请求的。
  4. 历史槽(History slot):之前的对话轮次。此槽位在会话期间会增长,需要主动管理——截断策略、摘要或固定窗口。
  5. 查询槽(Query slot):当前的客户输入。与其他槽位隔离,因此不会被误认为是指令。

当这些区域被明确定义,而不是随意的约定俗成时,许多事情就变得可行了。上下文组装代码变成了一个具有清晰输入输出的函数。每个槽位可以独立进行版本管理。槽位内容可以单独记录和追踪。事故分析可以说“由于检索缓存过期,上下文槽位包含了陈旧文档”,而不是“模型说了些错误的话”。

还有一个关于位置的实际考量。语言模型中关于序列位置效应(serial position effects)的研究表明,埋在长上下文中间的信息所获得的关注显著低于开头或结尾的内容——在某些基准测试中,从中间检索的性能会下降 20%。基于槽位的布局使得推理和控制内容的存放位置变得容易。如果最关键的指令位于顶部的系统槽中,那是刻意的架构决策。如果它们因为增量增长而处于一个 200 行提示词的第 47 行,那就是一个迟早会出问题的意外。

让 Diff 更清晰易读

衡量 Prompt 结构好坏的标准是,对 Prompt 变更的代码审查(Code Review)是否能像代码 Diff 一样传达意图。

XML 标签是实现这一目标最常用的技术,理由很充分——它们显式、自描述,并能防止“上下文污染”(Context Contamination),即一个部分的内容意外影响模型对另一部分的解释:

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates