第 5 课:Context Engineering——Compaction 与 Prompt View
上一课解决了会话怎样保存。现在,排查助手继续工作,日志和对话越来越长:原始记录都要留着,但每次请求真的需要把它们全部发给模型吗?
如果只是找一处报错,却每次重读欢迎语、启动信息和重复警告,模型收到的就像一箱未拆封的快递。本课把输入拆开整理:先处理过大的工具结果,再决定是否压缩旧历史。
1. Artifact 与 Prompt View:保留证据,选择输入
围绕任务组织模型本次可见的材料,叫 Context Engineering(上下文工程)。在排查助手中,先区分三份东西:
Artifact
原始日志、文件等完整产物
保存可以再次查阅的证据
Transcript
用户、模型与工具的消息和相关事件
保留会话过程
Prompt View
本轮选取、整理后的会话消息
进入接下来的模型请求
本课代码中的 Prompt View 只表示会话部分,固定指令和工具说明还要加入完整请求。存储里有一份日志,不代表每次都要把全部内容送进去。
下面把日志缩成四行,只演示“原文保留,取末尾两行作为预览”,不是实际采集的完整日志:
full_output = "服务启动\n收到 order_404 请求\nTypeError: 'NoneType' object is not subscriptable\n返回 HTTP 500"
lines = full_output.splitlines()
preview = "\n".join(lines[-2:])
print(preview)
print("完整输出仍有", len(lines), "行")输出为:
TypeError: 'NoneType' object is not subscriptable
返回 HTTP 500
完整输出仍有 4 行lines[-2:] 取最后两行,不会修改原始字符串。真实工具还要保留完整产物,并提供继续读取的位置;若根因在日志中间,只看末尾就会漏掉证据。
同样,旧 Tool Result 也可以只在本轮输入里缩短,磁盘上的原始记录保持不变。这种请求前的修剪通常叫 Pruning。[1] 内容可以缩短,工具消息的角色、调用编号和对应关系仍须保留。
2. Token Budget 与 Compaction:先计量,再决定
会话继续增长后,只裁剪单份日志可能还不够。需要先计算下一次请求的空间需求,再决定怎样处理历史。
模型通常按 Token 计量输入输出。Token、字符数和字节数不是同一单位,不能把“200KB 日志”直接换算成确定的 Token 数。
假设模型的共享上下文窗口为 32,000 Token,下面全部使用给定的教学数字:
结果是 35000 和 True。固定指令、工具定义、历史、新结果、输出预留都算进去以后,需要至少回收 3,000 Token,或者调整本轮请求。这不是 Tokenizer 实测;接真实模型时,应按对应模型的规则计量,并核对输入和输出限制。
如果旧历史占了大头,可以用摘要替代较早内容,保留近期原文。这个过程叫 Compaction(压缩)。原始 Transcript 仍保留;压缩后也要重新算预算,因为新摘要本身同样占空间。
本书核验的 Pi 版本会预留窗口空间、压缩较早历史并保留近期消息。[2] 关键是给下一步留下空间,不是等请求已经超限了才临时删几行。
3. 压缩边界:完整 Turn 与工具消息配对
历史不是可以任意切开的长字符串。先看一个只调用一次工具的完整 Turn:
把调用申请留在一边、结果留在另一边,模型就可能收到一张没有回执的申请,或一份找不到申请的结果。同一条 Assistant 消息提出多个工具调用时,相应结果也必须成组保留。
基础策略先在完整 Turn 之间切分。假设当前会话有三轮:
T1:确认排查目标和只读范围
已完成
可以摘要
T2:读取日志并给出初步解释
已完成
保留原文
T3:申请读取源码,结果未返回
尚未完成
保留当前状态
切点在 T1 之后。T2 不被拆开,T3 也不会因为“太长了”就被抹掉。没有可压缩的旧轮次,就不能为了凑长度硬切。
保留未完成轮次,不等于可以立即把它发给模型。 如果 T3 还有未完成的工具申请,应先完成执行或恢复处理、补齐对应 Tool Result,再发起下一次请求。
更复杂的实现可以在轮内已经闭合的工具交互边界切分,称为 Split Turn。它还要保住该轮的用户目标。本课先使用完整轮次边界,不把两种策略混成同一个简单规则。
摘要也需要检查。比如待压缩历史包含了排查目标和只读约束:
摘要应保留目标、约束、已确认事实和未完成事项。只能写出源记录支持的结论;“尚未核对源码”不能变成“已经修复”。检查长度或几个关键词,不能证明语义完整。
4. Prompt View:最新摘要、保留尾部与新增消息
写下压缩记录以后,内存中的旧消息列表不会自己变短。Harness 必须在请求前重新组装输入:
retained_tail 是明确保留下来的原文消息列表。它不是“再读一遍所有历史”,也不是又生成一份摘要。
这份保存摘要和保留尾部的压缩记录,是用于重建会话输入的一种 Checkpoint;应用的其他运行状态可以另外保存。
下面用普通文本消息展示拼接;摘要是给定数据,这段代码不调用摘要模型:
输出为:
extend() 把列表中的消息逐条加入。模型接收这些展开后的消息,不是 checkpoint 的 JSON 外壳;固定指令和工具说明仍由调用方加入。实际工具消息要保留调用编号和配对结果,摘要也不能把日志中的文字提升为系统指令。
随着后续轮次完成,第二次压缩还要接住第一次的摘要:
漏掉旧摘要,T1 的信息就会离开未来输入;把所有旧摘要和已保留的尾部重复发出,又会让输入重新膨胀。存储中可以追加多条 Compaction 记录,但组装当前视图只使用最新一条及其后续消息。
5. 失败处理与 Prompt Cache:控制压缩代价
先生成候选摘要,确认可用后再保存新的压缩记录。摘要请求失败、返回空白,或新视图反而更长时,不应把它当作成功的压缩点。
原记录要保留。如果旧输入仍超预算,可以缩减本轮工具输出、调整材料或暂停处理,不能忽略压缩失败,继续把超长请求原样发出去。压缩记录的写入也须确认成功,损坏记录应显式报错;保留原历史不代表写入故障已经自动修复。
输入变短也不保证更便宜。有些模型服务会复用相同输入前缀的计算,称为 Prompt Cache。缓存的是计算,不是替应用保存会话。
反复改写靠前的历史,可能让后面一大段稳定前缀无法复用。再加上生成摘要本身的请求和等待,少发了一些 Token,总费用或耗时却可能增加,具体取决于服务规则和实际命中情况。[3]
因此,压缩后至少检查两类结果:输入空间是否够用,关键目标和约束是否仍在;再结合摘要开销、后续耗时和缓存命中,决定压缩频率。不要只追求消息越来越短。
也可以使用模型服务提供的压缩能力。[4][5] 无论摘要由谁生成,应用仍要区分完整记录、当前输入与长期 Memory;临时日志不会因为即将被压缩,就自动成为值得长期保存的事实。
本章先控制模型每次看见什么。下一课准备把助手从只读扩展到受限写入:如果文件已经改了,回执却没保存,恢复会话还不足以决定要不要再做一次。
本课实践
进入第 5 课实践:观察压缩前后的输入,改变保留轮数,解释原始历史与当前视图的区别。说明中列出必做步骤、完成标准和选做内容。
资料
Last updated
Was this helpful?