pi-distill
v1.7.0
Published
Pi tool-output distillation with file-first configuration
Maintainers
Readme
pi-distill
保留事实,把上下文留给决策。
pi-distill 是一个 Pi 扩展:它不替换工具,也不改变命令的执行方式,只在工具已经返回真实结果之后,帮助 Agent 决定哪些内容值得进入下一轮上下文。
解决什么问题
编码 Agent 通常只需要命令、搜索或文件读取结果中的关键信息。把大段日志、生成文件或搜索结果完整塞入下一轮,会增加上下文消耗,也容易让有效信号被噪声淹没。pi-distill 在不替换 Pi 内置工具的前提下,增加一层结果级提炼。
实际上下文节省效果
构建日志、diff 输出和测试报告经常包含重复状态行、未变化上下文、堆栈噪声,以及下一步决策并不需要的细节。这些内容通常很适合高比例压缩。下面这张真实 Pi 会话截图中,结果从 51,215 个字符压缩到 240 个字符:213.40 倍压缩,输出字符减少 99.5%。

截图统计的是字符减少比例,不是 tokenizer 得出的精确 token 统计。实际使用时通常会带来同量级的上下文 token 节省,但精确数值取决于语言、内容和模型 tokenizer。对于适合压缩的冗长输出,90% 以上是已经观察到的效果,但不是每个命令的保证;需要完整输出时请使用 RAW。
| 场景 | 常见噪声 | 提炼结果保留 | | --- | --- | --- | | 构建 / 编译 | 重复进度、警告和未变化的环境信息 | 成功/失败、首个可行动错误、受影响文件和后续步骤 | | Diff 检查 | 大量未变化 hunk 和格式化噪声 | 变更文件、相关 hunk 和评审所需事实 | | 测试 | 单测逐条输出、snapshot 和框架模板 | 总数、失败用例、关键断言和有效诊断 |
Prompt 语言
提炼 prompt 会严格跟随 /config:language 当前选择的语言。持久化语言发生变化后,下一次工具调用会读取新设置,即使语言命令和 pi-distill 来自不同的包实例也可以同步。PI_EXTENSIONS_LOCALE 仍然是显式的环境变量覆盖项。原始用户消息只作为语言上下文传入,不能覆盖已选择的语言。
工作方式
- 通过 Pi 原生的
tool_call/tool_result事件监听bash、read、grep和find。 - 以工具的
outputRequest作为是否提炼、如何提炼的依据。 - 当提示词严格只有
RAW时,视为明确要求返回原始输出。 - 默认使用当前会话模型,也可以配置独立的
provider/model。 - 在工具结果 details 中保留状态、字符数、压缩比、耗时和异常等诊断信息。
- 超长输出不再由 pi-distill 写文件或截断,统一交由 Pi 自身的输出限制机制处理。
- 当前 Pi 展示中间件可用时显示紧凑审计卡片,否则使用自己的 fallback renderer。展示协议由公共运行库
pi-extensions-tool-display提供。
它不会注册第二个 bash、read、grep 或 find 工具。
安装
pi install npm:pi-distill包清单会把共享依赖 pi-extensions-i18n 和 pi-extensions-tool-display 作为扩展入口加载,不需要额外安装这些包。安装 pi-distill 后即可使用 /config:language。
安装后重新加载 Pi:
/reload交互式配置命令:
/config:distill核心思想
我们不是想让 Agent 少看信息,而是避免它为了找一句结论,被迫把几千行日志一起带进上下文。
工具执行层需要保留完整事实;Agent 消费层需要控制上下文成本。pi-distill 在两者之间增加一个可选的结果处理层:
- 工具负责执行并返回事实;
- Agent 通过
outputRequest表达自己关心什么; - 扩展读取真实输出后,再决定是否调用提炼模型;
- 模型只压缩消费路径,不改变原工具的业务语义;
- 诊断信息记录这次处理是否真的节省了上下文。
因此,提炼不是“把所有输出都交给模型总结”,而是一份明确的工具契约:需要什么就提取什么,需要完整内容就保留原文。
为什么需要它
构建、测试和 diff 往往会返回大量重复状态、未变化上下文、框架模板和堆栈噪声。Agent 可能只需要失败原因、变更文件或最终状态,却被迫先消费整段输出。
直接截断会丢失关键事实;新增一个总结工具会增加调用链和决策负担;等 Agent 看完再总结又已经消耗了上下文。pi-distill 选择在结果进入后续推理前处理它,同时保留明确的原文模式和失败回退。
实际效果
下面是一段真实 Pi 会话中的输出:原始结果从 51,215 个字符提炼到 240 个字符,压缩 213.40 倍,输出字符减少 99.5%。

这张图统计的是字符减少比例,不是 tokenizer 得出的精确 token 数。实际 token 节省会受到语言、内容和模型 tokenizer 影响;对于适合压缩的构建日志、diff 和测试输出,90% 甚至更高的节省比例是已经观察到的结果,但不是每个命令的保证。
| 场景 | 原始输出中的典型噪声 | 提炼后优先保留 | | --- | --- | --- | | 构建 / 编译 | 重复进度、环境信息、重复警告 | 成功/失败、首个可行动错误、受影响文件、后续步骤 | | Diff 检查 | 大量未变化 hunk、格式化噪声 | 变更文件、相关 hunk、评审所需事实 | | 测试 | 逐条单测输出、snapshot、框架模板 | 总数、失败用例、关键断言、有效诊断 |
节省比例不是唯一指标。扩展还记录提炼耗时、原始字符数、结果字符数、压缩比和异常;如果总结没有带来真实收益,会暴露 ineffective-compression,而不是静默假装优化成功。
工作原理
一次工具调用的处理链路如下:
Agent 提出处理目标
↓ 通过 outputRequest 传给工具
工具执行真实操作,返回 stdout / stderr / 文件内容 / 多媒体结果
↓
pi-distill 根据真实结果和配置决定:原样返回,或调用模型提炼
↓
Agent 消费更适合当前决策的结果,并获得可审计的处理诊断- 扩展在会话启动时为所有已启用、参数 schema 为 object 的工具增加必填的
outputRequest参数;edit和write默认关闭,其他未配置工具默认开启。不写死bash、read、grep或find。 tool_call事件捕获这个参数,并在交给底层工具前移除它,因此原工具不会收到扩展专用字段。tool_result事件拿到真实输出后再做判断,不依赖 Agent 对输出长度的预测。- 每次工具调用都必须包含非空的
outputRequest;严格的RAW表示明确要求原文;其他非空 prompt 才允许进入提炼流程。 - OpenAI-compatible Completions 提炼请求会通过
response_format: { "type": "json_object" }启用原生 JSON 模式;OpenAI Responses-compatible 请求使用等价的text.format。单次提炼超时后按照timeoutRetryCount重试,其他模型调用异常按照errorRetryCount重试(两者默认都重试 1 次);如果模型已经返回文本,但只是 JSON 语法或响应结构校验失败,扩展会把坏响应和校验错误交给一次 JSON-only 修复 prompt,不会再次发送工具输出,也不会重新总结;修复失败、没有可用模型或结果收益过低时,扩展保留原始事实,并通过 details 和审计卡片暴露状态;模型用 Markdown 的 JSON 代码围栏(如json …)包裹响应时也会兼容解析。
输出处理契约
| outputRequest | 行为 | 适用场景 |
| --- | --- | --- |
| 未提供 | 工具调用无效;Pi 会在底层工具执行前拒绝该调用 | 不要省略;未明确要求压缩时使用 RAW |
| 严格为 RAW(大小写不敏感) | 不调用提炼模型,保留完整原始文本;超长时由 Pi 自身的输出限制机制处理 | 逐字核对、复制内容、需要完整日志时 |
| 任意非空且非 RAW | 输出达到阈值后调用模型,超时与其他异常分别使用独立重试次数,具体保留内容由 prompt 决定 | “只保留错误、警告和最终状态”等场景 |
| 包含图片、音频或其他非文本内容 | 原样保留,不发送给提炼模型,不做文本长度截断 | 图片读取、二进制结果、混合文本与图片结果 |
RAW 是确定性的完整输出信号。提炼 prompt 会要求总结模型在用户明确要求“不遗漏地完整提取”时直接返回 RAW,尤其适用于语法、参数、SQL、API 调用或其他需要复制的精确文本。工具调用方可以控制参数时,直接传 RAW 仍然是首选方式。
Prompt 语言
提炼 prompt 完全跟随 /config:language 当前选择的语言:
- 切换语言后,下一次工具调用读取新的持久化语言设置;
- 即使
/config:language和pi-distill来自不同的包实例,也通过共享 locale 设置同步; PI_EXTENSIONS_LOCALE可以作为显式环境变量覆盖;- 原始用户消息只作为任务上下文传入,不会把中文用户消息误判成中文 prompt。
覆盖范围与边界
- 自动处理所有当前已启用且参数 schema 为 object 的工具;能否注入
outputRequest由工具 schema 决定,不维护固定工具名单。 - 不注册替代工具,不改变原工具的执行语义,也不要求额外安装独立的
pi-tool-display宿主包。 - 文本提炼是有损操作;完整性要求应使用
RAW。 - 非文本结果是完整性边界:图片、音频、二进制和混合 content 不进入文本提炼链路。
- 超长输出不再由 pi-distill 写临时文件或截断,统一交由 Pi 自身的输出限制机制处理,避免上下文无限膨胀。
- 当前会话没有模型时,提炼会失败并保留原始结果,不阻止 Pi 启动。
配置
默认配置路径:
~/.pi/agent/extensions/pi-distill/config.json可以从 config.example.json 开始:
{
"enabled": true,
"model": "",
"minChars": 200,
"maxChars": 100000,
"maxOutputChars": 10000,
"timeoutSeconds": 10,
"timeoutRetryCount": 1,
"errorRetryCount": 1,
"missedCompressionRatio": 10,
"summarizeErrors": true,
"tools": {},
"render": {
"enabled": true,
"showPrompt": true,
"showResult": true
}
}配置文件字段优先于环境变量。未声明的字段依次回退到 PI_DISTILL_*、旧版 PI_BASH_SUMMARY_* 变量和默认值。
| 配置项 | 含义 |
| --- | --- |
| model | 可选的 provider/model;为空时使用当前 Pi 会话模型。 |
| minChars | 达到此输出长度后才请求提炼。 |
| maxChars | 提炼结果超过此字符数时写入临时文件。 |
| maxOutputChars | 最终返回给 Agent 的文本上限。超出后写入临时文件,只返回文件指针。 |
| timeoutSeconds | 每次提炼模型尝试的最长等待时间。 |
| timeoutRetryCount | 超时后的额外重试次数。默认 1;设为 0 时不重试超时。 |
| errorRetryCount | 非超时异常后的额外重试次数。默认 1;设为 0 时不重试其他异常。 |
| missedCompressionRatio | 没有提供摘要 prompt 时,用于长输出诊断的倍数阈值。 |
| summarizeErrors | 工具返回错误且达到 minChars 时,是否仍发送给提炼模型。 |
| tools.<name>.enabled | 按工具开启或关闭 outputRequest 注入和结果提炼。edit 和 write 默认关闭,其他未配置工具默认开启,也可以通过 /config:distill 修改。 |
/pi-distill 仍作为兼容别名保留。
| render.* | 控制审计卡片、prompt 预览和结果预览。 |
Session 统计
使用 /distill:stats 查看当前 Pi 会话的提炼统计。统计只保存在内存中,在会话开始时重置,不保存原始工具输出。
统计包括工具结果数量、成功/失败/回退次数、模型尝试次数、原始与摘要字符数、压缩比、估算的原文/摘要 Token(启发式:CJK 字符约每字 1 token,其余文本约 4 字符 1 token)、预计节省 Token、提炼实际消耗的 input/output/cache/total Token 和成本。数量达到 1,000 或 1,000,000 时分别使用 k 或 m 紧凑显示;耗时会根据数值显示为 ms、s 或 min。原文/摘要 Token 是估算值;provider 未返回 usage 时,提炼消耗 Token 或成本字段显示为不可用。
主要环境变量包括 PI_DISTILL_MODEL、PI_DISTILL_MIN_CHARS、PI_DISTILL_MAX_CHARS、PI_DISTILL_MAX_OUTPUT_CHARS、PI_DISTILL_TIMEOUT_SECONDS、PI_DISTILL_TIMEOUT_RETRY_COUNT、PI_DISTILL_ERROR_RETRY_COUNT、PI_DISTILL_MISSED_COMPRESSION_RATIO 和 PI_DISTILL_SUMMARIZE_ERRORS。
要求
- Node.js 22 或更高版本。
- 当前 Pi 会话需要有可用模型,除非
model指向一个已配置且可用的模型。
