> For the complete documentation index, see [llms.txt](https://levon.gitbook.io/agent-engineering/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://levon.gitbook.io/agent-engineering/05-shang-xia-wen-gong-cheng.md).

# 第 5 课：Context Engineering——Compaction 与 Prompt View

上一课解决了会话怎样保存。现在，排查助手继续工作，日志和对话越来越长：原始记录都要留着，但每次请求真的需要把它们全部发给模型吗？

如果只是找一处报错，却每次重读欢迎语、启动信息和重复警告，模型收到的就像一箱未拆封的快递。本课把输入拆开整理：先处理过大的工具结果，再决定是否压缩旧历史。

## 1. Artifact 与 Prompt View：保留证据，选择输入

围绕任务组织模型本次可见的材料，叫 **Context Engineering（上下文工程）**。在排查助手中，先区分三份东西：

| 名称          | 对应什么             | 用途          |
| ----------- | ---------------- | ----------- |
| Artifact    | 原始日志、文件等完整产物     | 保存可以再次查阅的证据 |
| Transcript  | 用户、模型与工具的消息和相关事件 | 保留会话过程      |
| Prompt View | 本轮选取、整理后的会话消息    | 进入接下来的模型请求  |

本课代码中的 Prompt View 只表示会话部分，固定指令和工具说明还要加入完整请求。存储里有一份日志，不代表每次都要把全部内容送进去。

下面把日志缩成四行，只演示“原文保留，取末尾两行作为预览”，不是实际采集的完整日志：

```python
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，下面全部使用给定的教学数字：

```python
window = 32000
fixed_input = 4000
history = 23000
new_tool_result = 3000
output_reserve = 4000
safety_margin = 1000

required = (
    fixed_input + history + new_tool_result
    + output_reserve + safety_margin
)
print(required)
print(required > window)
```

结果是 `35000` 和 `True`。固定指令、工具定义、历史、新结果、输出预留都算进去以后，需要至少回收 3,000 Token，或者调整本轮请求。这不是 Tokenizer 实测；接真实模型时，应按对应模型的规则计量，并核对输入和输出限制。

如果旧历史占了大头，可以用摘要替代较早内容，保留近期原文。这个过程叫 **Compaction（压缩）**。原始 Transcript 仍保留；压缩后也要重新算预算，因为新摘要本身同样占空间。

本书核验的 Pi 版本会预留窗口空间、压缩较早历史并保留近期消息。\[2] 关键是给下一步留下空间，不是等请求已经超限了才临时删几行。

## 3. 压缩边界：完整 Turn 与工具消息配对

历史不是可以任意切开的长字符串。先看一个只调用一次工具的完整 Turn：

```
User -> Assistant Tool Call -> Tool Result -> Assistant Final
```

把调用申请留在一边、结果留在另一边，模型就可能收到一张没有回执的申请，或一份找不到申请的结果。同一条 Assistant 消息提出多个工具调用时，相应结果也必须成组保留。

基础策略先在完整 Turn 之间切分。假设当前会话有三轮：

| 轮次              | 当前状态 | 保留最近一轮完整对话时 |
| --------------- | ---- | ----------- |
| T1：确认排查目标和只读范围  | 已完成  | 可以摘要        |
| T2：读取日志并给出初步解释  | 已完成  | 保留原文        |
| T3：申请读取源码，结果未返回 | 尚未完成 | 保留当前状态      |

切点在 T1 之后。T2 不被拆开，T3 也不会因为“太长了”就被抹掉。没有可压缩的旧轮次，就不能为了凑长度硬切。

**保留未完成轮次，不等于可以立即把它发给模型。** 如果 T3 还有未完成的工具申请，应先完成执行或恢复处理、补齐对应 Tool Result，再发起下一次请求。

更复杂的实现可以在轮内已经闭合的工具交互边界切分，称为 **Split Turn**。它还要保住该轮的用户目标。本课先使用完整轮次边界，不把两种策略混成同一个简单规则。

摘要也需要检查。比如待压缩历史包含了排查目标和只读约束：

```
不足：之前讨论了订单问题。

可用：目标是排查订单接口的 500，只读，不修改文件。
      日志位置为 server.log，后续诊断必须引用证据。
```

摘要应保留目标、约束、已确认事实和未完成事项。只能写出源记录支持的结论；“尚未核对源码”不能变成“已经修复”。检查长度或几个关键词，不能证明语义完整。

## 4. Prompt View：最新摘要、保留尾部与新增消息

写下压缩记录以后，内存中的旧消息列表不会自己变短。Harness 必须在请求前重新组装输入：

```
本轮会话输入 = 最新摘要 + retained_tail + 此后新增的消息
```

`retained_tail` 是明确保留下来的原文消息列表。它不是“再读一遍所有历史”，也不是又生成一份摘要。

这份保存摘要和保留尾部的压缩记录，是用于重建会话输入的一种 Checkpoint；应用的其他运行状态可以另外保存。

下面用普通文本消息展示拼接；摘要是给定数据，这段代码不调用摘要模型：

```python
checkpoint = {
    "summary": "只读排查订单接口的500，日志为 server.log，诊断要引用证据。",
    "retained_tail": [
        {"role": "user", "content": "日志里有什么异常？"},
        {"role": "assistant", "content": "发现 TypeError，尚未核对源码。"},
    ],
}
new_messages = [{"role": "user", "content": "读取 order_service.py 核对原因"}]

messages = [{
    "role": "assistant",
    "content": "历史摘要：" + checkpoint["summary"],
}]
messages.extend(checkpoint["retained_tail"])
messages.extend(new_messages)

for message in messages:
    print(message["role"], message["content"])
```

输出为：

```
assistant 历史摘要：只读排查订单接口的500，日志为 server.log，诊断要引用证据。
user 日志里有什么异常？
assistant 发现 TypeError，尚未核对源码。
user 读取 order_service.py 核对原因
```

`extend()` 把列表中的消息逐条加入。模型接收这些展开后的消息，不是 `checkpoint` 的 JSON 外壳；固定指令和工具说明仍由调用方加入。实际工具消息要保留调用编号和配对结果，摘要也不能把日志中的文字提升为系统指令。

随着后续轮次完成，第二次压缩还要接住第一次的摘要：

```
第一次：摘要 T1，保留 T2
第二次：摘要“上一份摘要 + T2”，保留 T3
```

漏掉旧摘要，T1 的信息就会离开未来输入；把所有旧摘要和已保留的尾部重复发出，又会让输入重新膨胀。存储中可以追加多条 Compaction 记录，但组装当前视图只使用最新一条及其后续消息。

## 5. 失败处理与 Prompt Cache：控制压缩代价

先生成候选摘要，确认可用后再保存新的压缩记录。摘要请求失败、返回空白，或新视图反而更长时，不应把它当作成功的压缩点。

原记录要保留。如果旧输入仍超预算，可以缩减本轮工具输出、调整材料或暂停处理，不能忽略压缩失败，继续把超长请求原样发出去。压缩记录的写入也须确认成功，损坏记录应显式报错；保留原历史不代表写入故障已经自动修复。

输入变短也不保证更便宜。有些模型服务会复用相同输入前缀的计算，称为 **Prompt Cache**。缓存的是计算，不是替应用保存会话。

反复改写靠前的历史，可能让后面一大段稳定前缀无法复用。再加上生成摘要本身的请求和等待，少发了一些 Token，总费用或耗时却可能增加，具体取决于服务规则和实际命中情况。\[3]

因此，压缩后至少检查两类结果：输入空间是否够用，关键目标和约束是否仍在；再结合摘要开销、后续耗时和缓存命中，决定压缩频率。不要只追求消息越来越短。

也可以使用模型服务提供的压缩能力。\[4]\[5] 无论摘要由谁生成，应用仍要区分完整记录、当前输入与长期 Memory；临时日志不会因为即将被压缩，就自动成为值得长期保存的事实。

本章先控制模型每次看见什么。下一课准备把助手从只读扩展到受限写入：如果文件已经改了，回执却没保存，恢复会话还不足以决定要不要再做一次。

## 本课实践

[进入第 5 课实践](broken://pages/HtjbocAIJVgpLeFYLeuM)：观察压缩前后的输入，改变保留轮数，解释原始历史与当前视图的区别。说明中列出必做步骤、完成标准和选做内容。

## 资料

1. [OpenClaw：已核验版本的请求前修剪](https://github.com/openclaw/openclaw/blob/ad3268ecccbc7758878662b31adcd72475343d3e/docs/concepts/session-pruning.md)
2. [Pi：已核验版本的 Compaction](https://github.com/earendil-works/pi/blob/e266507b606b9552fa277252644054afd4384b11/packages/coding-agent/docs/compaction.md)
3. [Hermes：Micro-compaction 与缓存权衡](https://github.com/NousResearch/hermes-agent/blob/afc3d9d34c9c3b01fa2e1332d2c66a5b5fabae3f/docs/micro-compaction.md)
4. [OpenAI：Compaction](https://developers.openai.com/api/docs/guides/compaction)
5. [Anthropic：Compaction](https://platform.claude.com/docs/en/build-with-claude/compaction)
6. [固定源码及全部实现对照](https://github.com/unix2dos/agent-engineering-book/tree/main/research/01-05-chapter-promotion-sources.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://levon.gitbook.io/agent-engineering/05-shang-xia-wen-gong-cheng.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
