Skip to content
工具箱

上下文工程

大语言模型的注意力机制存在 n² 的复杂度关系:输入序列长度翻倍,注意力计算量翻四倍。更关键的是,token 越多,模型的注意力越稀释——“长上下文”不等于”好理解”。

维度短上下文(~10K tokens)长上下文(~100K tokens)
API 延迟1-3 秒5-15 秒
输入费用token 数 × 单价
注意力质量精准,关键信息密度高稀释,模型可能”遗忘”中间部分
缓存命中维护简单缓存碎片化,命中率下降

核心思路:把上下文当作有限资源管理,而非无限垃圾桶。

Peri 的设计哲学就是帮你管理这一资源——系统提示词冻结缓存、按需文件加载、自动压缩、子代理隔离——全是为了用最少的 token 传达最精准的信息。

传统 AI 编码助手每次请求都发送完整的系统提示词。一个典型的编码助手系统提示词可能占用 8K-15K tokens——每次 /turn 都原样发送,这些 8K-15K token 反复计费。

sequenceDiagram
    participant U as 用户
    participant A as 传统助手
    participant API as API

    U->>A: 第1轮对话
    A->>API: System(15K) + Context + 本轮输入 → 计15K输入
    U->>A: 第2轮对话
    A->>API: System(15K) + 全部历史 + 本轮输入 → 又计15K
    U->>A: 第3轮对话
    A->>API: System(15K) + 全部历史 + 本轮输入 → 又计15K
    Note over A,API: 10轮对话累计 150K 系统提示词 token 费用

Peri 利用 Anthropic 的 Prompt Caching API,将系统提示词(包括 CLAUDE.md、skill 上下文、hook 指令等)作为**冻结前缀(frozen prefix)**缓存。这些 token 在会话生命周期内不变,95-99% 的对话 token 从缓存命中,不计费或按缓存价格计费。

sequenceDiagram
    participant U as 用户
    participant P as Peri
    participant API as API

    U->>P: 第1轮对话
    P->>API: [FROZEN System 15K] + 动态Context + 本轮输入
    Note over P,API: 缓存 MISS: 计15K(唯一一次)
    U->>P: 第2轮对话
    P->>API: [缓存命中 15K] + 动态Context + 本轮输入
    Note over P,API: 缓存 HIT: 15K 不计费或低费率
    U->>P: 第3-10轮对话
    P->>API: [缓存命中 15K] + 动态Context + 本轮输入
    Note over P,API: 每轮都在命中

以一个 15K tokens 的冻结前缀为例:

传统方式(10轮)Peri(10轮)
系统提示词 token 计费150K15K
缓存命中率0%95-99%
等效节省~90%

冻结前缀中的内容不会被压缩(compaction)修剪——它们是”永久上下文”。因此,写入 CLAUDE.md 的内容是真正的”免费指令”(仅首次收取缓存写入费)。

CLAUDE.md 不是文档 dump,而是浓缩指令——它占据冻结前缀,每次会话都要携带。你的每个字都在决定 Peri 将注意力分配到何处。

差的设计(信息 dump):

# 项目说明
本项目是一个用 Rust 编写的终端 AI 编码助手。使用 tokio 异步运行时,
ratatui 作为 TUI 框架,clap 处理命令行参数。项目结构分为 agent、
tui、tools、mcp 等模块。agent 负责 ReAct 循环,tui 负责界面渲染,
tools 负责工具定义和执行,mcp 负责与外部 MCP 服务器通信...
(继续 200 行项目介绍)

Peri 加载进来全是描述性信息,没有一个指令告诉它”该怎么做”。

好的设计(浓缩指令):

# 编码规范
- 禁止 println!,统一用 tracing(info/warn/error)
- CJK 截断用 chars().take(),禁止 bytes().take()
- 错误处理统一用 anyhow::Result,禁止裸 unwrap()
# 模块速查
| 模块 | 入口 | 关键规则 |
|------|------|----------|
| peri-agent | agent.rs | 中间件顺序不可变 |
| peri-mcp | client.rs | 连接超时 5 秒 |
# TRAP
- [TRAP] Windows 路径含反斜杠导致 glob 失败 → 统一 normalize 后再 glob

每一行都是可执行指令,Peri 不需要从中”提取信息”——直接遵守。

主 CLAUDE.md 放全局规则,细节用 <!-- @import path --> 引用展开(仅 CLAUDE.md 系列文件解析,AGENTS.md 不处理):

# 项目根 CLAUDE.md(< 50 行)
<!-- @import .claude/rules/coding-style.md -->
<!-- @import .claude/rules/testing.md -->

被引用的文件也参与冻结前缀缓存,但在文件层面分离,方便维护。路径相对于当前文件所在目录,递归深度上限 3。Peri 自身的 CLAUDE.md 就是这样做的大约 200 行主文件引用规则文件。

当对话历史接近模型的 token 限制时,Peri 触发自动压缩(compaction)。压缩器遍历对话历史,将已完成的操作总结为结构化摘要,丢弃原始的工具调用细节。

PreCompact 事件在压缩前触发——你可以通过 hook 在压缩前备份关键信息,或注入指令指定哪些内容必须保留。

PostCompact 在压缩后触发——验证压缩结果是否丢了关键上下文。

{
"hooks": {
"PreCompact": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "echo 'compaction开始' >> /tmp/peri-compaction.log"
}
]
}
]
}
}

压缩保留什么由内置规则决定,不需要(也无法)在 CLAUDE.md 中声明。Micro Compact 有一份黑名单工具列表,这些工具的消息(输入 + 输出)不参与截断

工具保留原因
Agent子任务描述等结构化参数不可恢复,丢失会导致子代理调度失败
AskUserQuestion用户答案不可恢复,丢失会导致对话断裂
goal长期目标状态,丢失会导致 agent 漂移方向
TodoWrite任务列表结构,丢失会导致工作记忆重置

此外,Smart Compact(遗留实现,默认关闭)会保留最近 5 条 User/Assistant 消息和最近 3 个工具调用结果。压缩完成后会追加续接指令 [Context has been compacted. Continue working based on the summary above.]

压缩后,Peri 的上下文变成一个”精简摘要 + 冻结前缀 + 最新几轮对话”的结构,既保留了项目知识,又释放了空间继续推理。

Peri 不会在会话开始时把所有项目文件读进上下文。它的工作方式是:

  1. 收到任务 → 分析需求
  2. 用 glob 发现文件 → 只获取匹配的文件路径
  3. 用 grep 定位代码 → 只读取匹配行附近的上下文
  4. 按需 Read → 只在需要完整内容时读取整个文件

每一步都只向上下文添加最少的信息。

flowchart TD
    T[任务: 修改用户认证逻辑] --> G[glob: **/auth*]
    G --> GR[grep: verify_token | login | authenticate]
    GR --> R[Read: 仅读匹配文件中的相关函数]
    R --> E[Edit: 精确修改]
    E --> V[验证: cargo check]

Peri 的工具输出自动截断——过长的 grep 结果、文件内容会被压缩为摘要。你不需要手动限制工具范围,Peri 内置了输出裁剪机制。

每个子代理拥有独立的上下文窗口,不会污染主对话。

flowchart LR
    M[主 Agent
上下文: 冻结前缀 + 摘要 + 最新对话] --> |派发任务| S1[子代理 1
独立上下文] M --> |派发任务| S2[子代理 2
独立上下文] M --> |派发任务| S3[子代理 3
独立上下文] S1 --> |结果摘要| M S2 --> |结果摘要| M S3 --> |结果摘要| M

这意味着:

  • 子代理执行过程中产生的大量搜索和读取不会挤占主对话的上下文
  • 主 Agent 收到的是子代理的结构化摘要,而非原始工具调用日志
  • 可以并行搜索多个代码区域而不互相干扰

子代理适合处理”搜索-分析-总结”类的密集型任务——它们吃掉 token,但只向主 Agent 返回提炼后的结论。

长时间任务(如重构 20 个模块、批量迁移测试)面临 token 积累问题。三种处理策略:

Peri 在压缩时会保留”结构化笔记”——你可以在任务开始前让 Peri 建立笔记文件:

在 notes/task-log.md 中创建任务日志。每完成一个模块,
记录:模块名、改动摘要、验证结果。压缩时优先保留此文件的内容。

压缩后 Peri 通过笔记文件恢复上下文,而非依赖对话历史。

将长任务拆成独立子代理,每个子代理处理一个相对独立的子任务。子代理完成后返回结果摘要,主 Agent 负责编排。

主 Agent:编排重构流程
├── 子代理 1:重构 src/models/(独立上下文,完成即返回摘要)
├── 子代理 2:重构 src/services/(独立上下文)
└── 子代理 3:更新测试(独立上下文)

子代理的内部对话历史在完成后销毁——主 Agent 只留下三份摘要。

两种策略可以组合:让每个子代理在自己的上下文中使用压缩,处理更大的子任务。主 Agent 用笔记文件协调全局状态。

拆分大任务。 一句”重构整个项目”会让 Peri 在单一上下文中同时处理几百行新老代码对比——注意力崩了。拆成”先重构模块 A → 验证 → 再重构模块 B”。

精简你的 CLAUDE.md。 每季度回顾一次——哪些规则 Peri 已经不再需要?哪些描述可以用 @import 引用拆出去?过了 200 行就考虑重构。

主动 /compact 不等到自动触发。当对话超过 15 轮时,主动执行 /compact 释放空间。压缩后确认 Peri 仍理解任务目标再继续。

给 Peri 明确的”当前关注点”。 每次 /clear/compact 后的第一条消息,先重申当前任务的目标和进度:

(续)当前在重构 auth 模块,已完成:
- JWT 签发逻辑迁移 ✓
- 中间件提取 ✓
下一步:迁移 refresh token 逻辑

这让 Peri 不需要从上下文中”猜测”当前状态。

分批改,分批交。 30 个文件的重构,不要一个 commit 搞定。每改 5-8 个文件,验证通过就 commit。如果某个批次出问题,损失可控,/clear 后也很容易恢复。