Pico 学习笔记 Harness 设计、历史压缩、三类目录与缓存复用

Pico 学习笔记(2026-09-22):Harness 设计、历史压缩、三类目录与缓存复用

主题:Harness 是什么 → 长历史怎么进上下文 → 相关笔记怎么召回 → sessions/runs/memory 三者的区别 → 短期工作记忆存哪 → prefix 缓存复用。

标记说明:【补充】 = 在原文基础上扩展的内容;【更正】 = 与源码不符、已按代码修正。


〇、写之前先想清楚的问题

这是我给自己定的"老规矩",也是这份笔记的出发点:

# 要点 一句话理解
1 全链路思维 + 架构设计图 拿到一个项目,先画整体架构图,别一头扎进函数
2 注重生产级异常处理和兜底 正常路径谁都会写,值钱的是出错时怎么办
3 锻炼 AI 协作能力、需求理解能力、方案设计能力 这三个能力比敲代码更难被替代
4 为什么这么设计、如何排错 真正值钱的是设计思维和排查能力,不是"我写了什么"
5 先想方案设计,再写代码 不是一上来就写代码
6 先从 harness 项目入手,拆解模块 不是一上来就学全链路技术栈;遇到不会的再针对性补
7 主动提自己踩过的坑 踩坑的价值 >> 实现的价值
8 你做了什么、解决了什么、为什么这么设计 三件套,缺一个都讲不完整
9 多问自己为什么 一个点能经得住多次 why,那就值得写
10 项目准备到 70% 就可以开始投递 让市场检验你的不足,面试本身就是学习的一环

一、Harness 是如何设计的

1.1 一句话定义

Agent = LLM + Harness。

  • 大模型负责:拿到信息、识别意图、决定下一步做什么。
  • Harness 负责:规定在什么边界内做、怎么记录、怎么验证、什么时候才算真的完成。

所以 Harness 是**"让 LLM 在一个受控环境里跑起来"的系统**。

类比:模型是司机 (会看路、会判断),Harness 是车本身------方向盘打多少度有极限、油表要记录、刹车必须有、到没到目的地得有人判定。没有车,司机再聪明也走不了。

1.2 Pico 的四层设计

层 解决的问题 对应模块
工具治理 不是看到工具调用就直接跑 先走白名单 → 检查工具是否存在 → 参数是否合法 → 是不是重复调用
memory + context manager 当前 prompt 里到底什么该进、什么不该进 context_manager.py / features/memory.py
run artifacts 过程和结果都要留下来 既能复盘,也能评测
benchmark 有了回归能力,改动才敢做 12 组上下文矩阵 / 12 memory 任务 / 10 恢复场景 / 12 回归任务

1.3 为什么叫它 Harness

把这几层合起来看,我想解决的是:能不能把一个 code agent 做成一个有状态、有边界、有验证、也有回归能力的工程系统。

这也是我最后把 Pico 定义成 harness 而不是"又一个 agent demo"的原因。


二、对话历史很长时,每轮怎么处理进入上下文的内容

ContextManager 面对长历史时,遵循三条原则:近窗优先 + 旧史压缩 + 从新到旧逐条加入。

原则 做法
1. 近窗优先 最近 6 条历史完整保留 ------ 下一步决策依赖刚刚发生的事
2. 旧史压缩 更早的内容做压缩:重复内容算一次、有摘要的用摘要、工具输出生成摘要行
3. 从新到旧逐条加入 逐条往预算里塞,直到超预算为止

2.1 超预算了怎么兜底

直接把整个原始历史截断到预算 ------ 这是最后的保险。

2.2 【补充】代码里的具体参数

位置 内容
context_manager.py:_render_history_section() recent_window = 6(第 316 行),recent_start = len(history) - 6
近窗条目 _render_history_item(item, 900) ------ 单条最多 900 字符
更早的 read_file 同路径只保留第一次 (collapsed_duplicate_reads);若 memory 里有该文件的摘要,直接渲染成 路径 -> 摘要 一行(reused_file_summary_count)
更早的其他工具 走 _summarize_old_tool_item() 生成摘要行(summarized_tool_count)
更早的其他消息 _render_history_item(item, 60) ------ 只留 60 字符
近窗也超预算 近窗条目按剩余空间截断(不是直接丢),保证"最近发生的事"至少留个尾巴
最终兜底 if len(rendered) > budget: rendered = _tail_clip(history_raw, budget)

细节 :_tail_clip(text, limit) 的实现是 text[:limit-3] + "..." ------ 它保留的是文本开头 。所以最后这道保险截出来的是历史的开头(最旧的部分),不是最近的。命名和实际行为有点反直觉,如果面试被问到"最后兜底保的是哪段",这是个能加分的小点。

2.3 【补充】还有第二个历史渲染器

runtime.py:history_text()(第 239 行)是另一处渲染逻辑,同样用 recent_start = len(history) - 6,只是近窗给 900、旧窗给 180。

两处窗口大小一致(都是 6),说明 6 是刻意选定的常量而不是随手写的数。


三、相关笔记的检索流程是怎么样的

3.1 流程

对当前用户请求分词后,遍历短期记忆(episodic_notes)和长期记忆(durable memory),按「标签命中 > 关键词重叠 > 时间新旧」排序,最终取前 3 条作为相关笔记。

3.2 这么粗糙的召回,会出什么问题

场景 例子 问题
1. 关键词碰巧重叠 当前任务"帮我修复登录 bug" / 召回笔记"用户登录过 GitHub,token 是 xxx" 都含"登录",但完全不相关
2. tag 太宽泛 当前任务"数据库配置在哪" / 召回笔记"database migration decided in 2025" tag 都是 database,但一个是"找配置",一个是"历史决策"
3. 长期记忆过期 当前任务"项目现在用什么数据库" / 召回笔记"Project uses MySQL" 半年前已迁到 PostgreSQL,旧笔记还在
4. 主语误匹配 当前任务"项目用什么语言" / 召回笔记"Project uses PostgreSQL" 主语都是 project,说的却是不同维度

3.3 为什么明知粗糙还是这么做

因为 Pico 现在更关注可解释性。 用 tag、关键词、时间新旧这三个维度,可以直观解释"这条为什么被召回",用于复盘和审计。

类比:这就像用明确的筛选条件("标签是 database")找人,而不是用"长得像"来找。前者你能说清楚为什么选中它,后者选错了你都不知道错在哪。

3.4 后续改进方向

  1. 加关键词重叠阈值 ------ 低于一定阈值的不召回;
  2. 加语义相似度召回。

3.5 【补充】排序键的精确形式与两级过滤

排序键是一个四元组,降序取 top-N:

复制代码
(exact_tag_match, keyword_overlap, recency, note_index)
字段 含义
exact_tag_match query 分词与笔记 tags 是否有交集(0/1)。这是最硬的一级
keyword_overlap query 分词与笔记正文/source/tags 分词的重叠个数
recency created_at 解析成时间戳,越新越大
note_index 笔记序号,打破平局用

并且有一道硬过滤 :if exact_tag_match == 0 and keyword_overlap == 0: continue ------ 两者全为 0 直接跳过,不参与排序。

3.6 【更正 + 重要缺陷】中文提问召不回任何笔记

这道"硬过滤"加上分词实现,产生了一个实测可复现的缺陷。

分词函数(features/memory.py:282):

python 复制代码
def _tokenize(text):
    return {token.lower() for token in re.findall(r"[A-Za-z0-9_]+", str(text))}

它只认 [A-Za-z0-9_]。 于是:

输入 分词结果 召回
帮我改一下权限模块的登录校验 空集 set() 0 条
update policy.py login check {update, policy, py, login, check} 1 条

根因链条 :中文 query → _tokenize 返回空集 → exact_tag_match = 0 且 keyword_overlap = 0 → 命中 continue → 所有笔记都被跳过。

影响 :纯中文输入下,relevant_memory 段恒为 - none------除非 query 里恰好带了 ASCII(文件名、函数名、变量名)。

修法(尚未实施):加中文 n-gram,或接 jieba 分词。

这个缺陷很适合放进面试复盘:不是算法不行,是分词口径没覆盖中文。一句话就能体现"我真的跑过、真的定位到根因了"。


四、sessions 和 runs 和 memory 有什么区别

假设你打开 Pico,进行了三次对话:

复制代码
第1次:pico "帮我看看 README"
第2次:pico "再帮我看看 config.py"
第3次:pico --resume latest "把数据库改成 MySQL"

4.1 sessions:对话历史,用于恢复

存什么:

json 复制代码
{
  "id": "session_20260929_143022",
  "messages": [
    {"role": "user", "content": "帮我看看 README"},
    {"role": "assistant", "content": "README 说项目用 PostgreSQL"},
    {"role": "user", "content": "再帮我看看 config.py"},
    {"role": "assistant", "content": "config.py 里有数据库连接串"},
    {"role": "user", "content": "把数据库改成 MySQL"},
    {"role": "assistant", "content": "已修改 config.py"}
  ],
  "created_at": "2026-09-29T14:30:22",
  "updated_at": "2026-09-29T15:10:05"
}

特点:

  • 跨轮次:三次对话都在同一个 session 里
  • 跨进程 :关掉 Pico 再打开,--resume latest 能接着聊
  • 完整事件流:所有 user / assistant / tool 消息

作用:

  • 让模型知道"之前聊过什么"
  • 支持 --resume latest 恢复对话

文件位置:

复制代码
.pico/sessions/
└── session_20260929_143022.json

4.2 runs:单次运行档案,用于复盘审计

存什么:

复制代码
.pico/runs/
├── run_20260929-143022-abc123/    ← 第1次 ask
│   ├── task_state.json            ← 任务状态
│   ├── report.json                ← 报告
│   └── trace.jsonl                ← 追踪日志
├── run_20260929-143500-def456/    ← 第2次 ask
│   ├── task_state.json
│   ├── report.json
│   └── trace.jsonl
└── run_20260929-151005-ghi789/    ← 第3次 ask
    ├── task_state.json
    ├── report.json
    └── trace.jsonl

task_state.json 内容:

json 复制代码
{
  "run_id": "run_20260929-143022-abc123",
  "task_id": "task_001",
  "user_request": "帮我看看 README",
  "status": "completed",
  "stop_reason": "final_answer_returned",
  "attempts": 2,
  "tool_steps": 1,
  "last_tool": "read_file",
  "final_answer": "README 说项目用 PostgreSQL",
  "checkpoint_id": "ckpt_001",
  "resume_status": "no-checkpoint"
}

trace.jsonl 内容(一行一个事件):

复制代码
{"event": "run_started", "created_at": "..."}
{"event": "prompt_built", "created_at": "...", "duration_ms": 1284}
{"event": "model_requested", "created_at": "..."}
{"event": "model_parsed", "created_at": "...", "duration_ms": 12}
{"event": "tool_executed", "created_at": "...", "duration_ms": 3}
{"event": "checkpoint_created", "created_at": "...", "trigger": "tool_executed"}
{"event": "run_finished", "created_at": "...", "run_duration_ms": 1310}

特点:

  • 每次 ask 一个 run:三次对话 → 三个 run
  • 完整运行档案:状态 + 追踪 + 报告
  • 不可变:run 一旦结束,就不再修改

作用:

  • 复盘:看某次运行发生了什么
  • 评测:benchmark 聚合多个 run
  • 调试:定位某次运行的问题

4.3 【更正】上面这两段有 4 处与源码不符

笔记最初是凭印象写的,对着代码逐条核了一遍:

# 原笔记写的 源码实际 依据
1 文件名 trace.log trace.jsonl run_store.py:30 trace_path()
2 事件字段 "event_type" "event" runtime.py:342 payload["event"] = event
3 task_state 里有 "created_at" 没有这个字段 ;实际还有 last_tool / checkpoint_id / resume_status task_state.py:97 to_dict()
4 "status": "success" "completed" (常量 STATUS_COMPLETED) task_state.py:12

另外 run 目录名用短横线 分隔:run_YYYYMMDD-HHMMSS-<6位hex>(task_state.py:44),不是下划线。

为什么值得较真:这些字段名在面试里被追问的概率不低("你怎么区分'怎么停的'和'停下时什么状态'?")。答错字段名,前面讲得再好也会被扣分。

顺带一个设计点:status 和 stop_reason 是分开存的两个字段 ------status 说"停下时是什么状态"(running / completed / stopped / failed),stop_reason 说"怎么停的"(final_answer_returned / step_limit_reached / retry_limit_reached / model_error / tool_timeout / approval_denied / delegate_failed / persistence_error / resume_load_error)。

4.4 memory:长期记忆

存什么:

复制代码
.pico/memory/
├── MEMORY.md                      ← 索引
└── topics/
    ├── project-conventions.md     ← 项目约定
    ├── key-decisions.md           ← 关键决策
    ├── dependency-facts.md        ← 依赖事实
    └── user-preferences.md        ← 用户偏好

特点:

  • 跨会话:关掉 Pico 再打开,记忆还在
  • 稳定事实:只存长期有效的信息
  • 按主题分类:4 个固定主题

作用:

  • 让模型"记住"项目长期事实
  • 减少重复读取文件
  • 跨会话保持一致性

4.5 【补充】"什么时候写进 memory"比想象中严格

原笔记写的是"任务成功后,从对话中提炼"。源码里其实有两道门:

第一道:意图门。 用户请求里必须出现意图词才会尝试沉淀(runtime.py:36-37):

语言 触发词
英文 capture / remember / save / store / persist / note
中文 记住 / 保存 / 记录 / 沉淀 / 长期记忆 / 持久记忆

第二道:格式门。 命中的行要匹配预设的行前缀才会被归类(runtime.py:38-46):

主题 行前缀(英文 / 中文)
project-conventions Project convention: / 项目约定:
key-decisions Decision: / 决策:
dependency-facts Dependency: / 依赖:
user-preferences Preference: / 偏好:

还有拒绝规则 (reject_durable_reason()):

拒绝原因 触发条件
empty 内容为空
secret_shaped 出现 api_key / token / secret / password 这类词,或 sk-xxxxxx 形态的字符串
transient_task_state 行以"当前目标 / 当前卡点 / 下一步 / 当前阶段 / 关键文件 / 已完成 / 已排除"开头 ------ 这是任务瞬时状态,不该进长期记忆
noisy_output 含 stdout / stderr / traceback / exit_code,或长度 > 220

还有一处需要更正 :promote_durable_memory() 不只在成功时调用------

调用点 时机
agent_loop.py:99 正常拿到 final answer 时
agent_loop.py:303 因 step limit / retry limit 停下的收尾分支

两条路径都会调。 所以准确说法是"每次 run 结束时都尝试沉淀",而不是"只有任务成功才沉淀"。

secret_shaped 这条挺值得说 ------ 长期记忆是跨会话持久化的,一旦把 token 写进去就永久留在磁盘上了。所以在"写入口"就拦一道,比事后清理靠谱得多。


五、三者对比

维度 sessions runs memory
存什么 对话历史 单次运行档案 长期记忆
粒度 会话级 任务级 事实级
生命周期 跨轮次、跨进程 单次 ask 跨会话
写入时机 每轮对话 每次 ask 每次 run 结束(需过意图门 + 格式门)
是否可变 持续追加 不可变 可更新(同主题旧值被 supersede)
内容类型 user / assistant / tool 消息 状态 + 追踪 + 报告 稳定事实
大小 可能很大 中等 精简
是否进 Prompt 部分(近窗 6 条 + 压缩后的旧窗) 否 按需召回(最多 3 条)
谁在用 模型 开发者 模型

一个容易忽略的点 :runs 完全不进 prompt。它只是"事后的证据",模型看不到。要进 prompt 的只有 sessions 的窗口和 memory 的召回结果。


六、三者的关系

复制代码
用户对话
    │
    ▼
┌─────────────────────────────────────────────┐
│ sessions(对话历史)                         │
│                                             │
│ 记录所有 user/assistant/tool 消息            │
│ 跨轮次、跨进程                               │
└─────────────────────────────────────────────┘
    │
    │ 每次 ask 产生一个 run
    ▼
┌─────────────────────────────────────────────┐
│ runs(运行档案)                             │
│                                             │
│ 记录单次运行的状态、追踪、报告                │
│ 每次 ask 一个 run                            │
└─────────────────────────────────────────────┘
    │
    │ run 结束时,按规则提炼关键信息
    ▼
┌─────────────────────────────────────────────┐
│ memory(长期记忆)                           │
│                                             │
│ 记录稳定的事实、决策、偏好                    │
│ 跨会话持久化                                 │
└─────────────────────────────────────────────┘

数据流:

  1. 用户对话 → 写入 sessions
  2. 每次 ask → 产生一个 run
  3. run 结束 → 按规则提炼 → 写入 memory

七、为什么要分三个目录

7.1 职责分离

目录 职责 谁用
sessions 对话连续性 模型(看历史)
runs 复盘和评测 开发者(看过程)
memory 长期知识 模型(看事实)

7.2 生命周期不同

  • sessions:跨轮次,持续追加
  • runs:单次 ask,不可变
  • memory:跨会话,可更新

7.3 用途不同

  • sessions:让模型知道"之前聊过什么"
  • runs:让开发者知道"这次运行发生了什么"
  • memory:让模型知道"项目的长期事实"

7.4 一句话总结

三次对话 → 一个 session(对话历史)+ 三个 runs(每次运行档案)+ 若干 memory(提炼出的事实)。

补一句源码里的原话(run_store.py 开头注释):"session.json 负责保存'可恢复的会话状态';RunStore 负责保存'单次运行的审计工件'。两者分开后,恢复现场和复盘证据不会混在一起。"

这就是这个设计最核心的动机:恢复 和审计是两件不同的事,混在一起会互相干扰。


八、长期记忆存在 .md 里,短期工作记忆存哪

8.1 短期工作记忆(state 字典)

短期工作记忆存 state["working"] 和 state["episodic_notes"]。

python 复制代码
state = {
    "working": {
        "task_summary": "当前任务的简短描述",          # 最多 300 字符
        "recent_files": ["最近接触的文件路径,最多 8 个"],
    },
    # 跨轮笔记,最多 12 条;每条含 text / tags / source / created_at / note_index / kind
    # 渲染时只显示条数,不展开正文
    "episodic_notes": [...],

    # 每个文件一个摘要,并记录 freshness(sha256),用来判断摘要是否过期
    "file_summaries": {...},
}

作用:

  • 告诉模型"当前在做什么任务"
  • 告诉模型"最近碰了哪些文件"
  • 支持相关笔记检索
  • 可以提炼为长期记忆

8.2 它存在内存里

  • Pico 运行时,这个字典在内存中
  • 每轮对话更新它
  • 程序退出后,内存释放

内存 vs 硬盘的区别:

操作方式
内存 变量、字典、列表,直接操作
硬盘 文件读写,需要 read_text() / write_text()

注意:短期工作记忆在物理上也会被写进 session.json (self.session["memory"] = self.memory.to_dict()),所以 --resume 之后还能接着用。它"短期"指的是生命周期随会话结束而终止,不是"不落盘"。

8.3 渲染时只给数量,检索时才拿正文

细节:短期记忆渲染时只渲染摘要,不渲染完整正文;正文只在检索时才按需拿出来。

① 渲染时只显示数量 (render_memory_text() 里是 - episodic_notes: {条数})

好处 说明
prompt 精简 不被无关笔记撑爆
模型有感知 看到"有 2 条笔记",知道"有记忆可用"
按需取用 具体内容等召回时再说

② 检索时按需拿内容

好处 说明
只拿相关的 不相关的笔记一条都不进 prompt
最多 3 条 控制 token
四级排序 优先最相关的

这套"先给目录、再取内容 "的做法,本质上是给记忆做了一次惰性加载:常驻 prompt 的只是"索引",真正花 token 的是被选中的那几条。


九、缓存复用:PromptPrefix

9.1 生效条件

PromptPrefix 带的三个指纹都不变时,prefix 可复用:

指纹 判断什么
hash prefix 文本是否变化
workspace_fingerprint 工作区是否变化
tool_signature 工具是否变化

9.2 失效(重建)条件

触发条件 是否重建
用户新增文件 ✅ 重建
用户修改文件 ✅ 重建
用户删除文件 ✅ 重建
工具 schema 变了 ✅ 重建
首次运行 ✅ 重建

缓存位置 :agent.prefix_state

9.3 收益一:省时间

复制代码
不缓存:
  每轮:扫描目录 + 算指纹 + 渲染工具 + 组装文本 + 算哈希
  总计:N × 单轮成本

缓存:
  第1轮:扫描目录 + 算指纹 + 渲染工具 + 组装文本 + 算哈希
  第2轮起:只检查指纹,不重建 prefix 文本

9.4 【更正】"第 2 轮起只要 5ms"这个说法不成立

原文假设"第 2 轮起只算工作区指纹 = 5ms",但看 refresh_prefix() 的实现,每轮都会重新构建一次 WorkspaceContext (runtime.py:219):

python 复制代码
def refresh_prefix(self, force=False):
    previous_hash = getattr(getattr(self, "prefix_state", None), "hash", None)
    previous_workspace_fingerprint = getattr(..., "workspace_fingerprint", None)

    # 工作区事实相对稳定,所以这里按整体刷新;
    # 只有这些事实真的变化了,才重建完整 prefix。
    refreshed_workspace = WorkspaceContext.build(self.root)          # ← 每轮都跑
    refreshed_workspace_fingerprint = refreshed_workspace.fingerprint()
    workspace_changed = force or refreshed_workspace_fingerprint != previous_workspace_fingerprint
    ...
    prefix_state = self.build_prefix() if workspace_changed or force or previous_hash is None else self.prefix_state

而 WorkspaceContext.build() 里要起 5 个 git 子进程 (workspace.py:72-99):

# 命令 用途
1 git rev-parse --show-toplevel 找仓库根
2 git branch --show-current 当前分支
3 git symbolic-ref --short refs/remotes/origin/HEAD 默认分支
4 git status --short 工作区状态(截断到 1500 字符)
5 git log --oneline -5 最近 5 条提交

外加读取 README 等项目文档(每份截断到 1200 字符)。

所以准确的说法是 :缓存复用省掉的是"重建 prefix 文本 + 渲染工具说明 + 算文本哈希 "这段;工作区扫描 + 指纹计算每轮照跑,而它才是耗时的大头(在 Windows 上尤其明显,起进程本身就贵)。

这个点如果面试被追问"那缓存到底省了多少",只答"省了重建 prefix"是站得住的;答"从 200ms 降到 5ms"就容易被反问"你测过吗"。实测值应该从 trace 里 prompt_built 事件的 duration_ms 取,那是代码自己记下来的。

9.5 收益二:省 token

如果两次请求的 prompt 前缀完全相同,模型服务端可以复用上次的计算结果,按折扣计费。

复制代码
请求1:
  prompt = [prefix 2000 tokens] + [user 100 tokens]
  → prefix 部分按正常价格计费
  → user 部分按正常价格计费

请求2:
  prompt = [prefix 2000 tokens] + [user 150 tokens]
  → prefix 部分命中缓存,按折扣价计费(通常 10%)
  → user 部分按正常价格计费

注意这里是两笔账 :本地复用省的是构建时间 ,服务端 Prompt Cache 省的是钱 。前者靠 hash 一致,后者靠 prompt_cache_key(代码里就直接用 prefix_state.hash)。

9.6 流程总结

Pico 的缓存复用流程是:每次 ask 时刷新工作区、算指纹、对比指纹;如果工作区没变,就复用上次的 prefix;否则重建。复用后,模型服务端能命中 Prompt Cache → 省 Token。

9.7 PromptPrefix 结构

python 复制代码
@dataclass
class PromptPrefix:
    text: str                      # prefix 文本内容
    hash: str                      # 文本的哈希
    workspace_fingerprint: str     # 工作区指纹
    tool_signature: str            # 所有工具信息打包后的哈希值
    built_at: str                  # 构建时间

tool_signature 打包的内容(prompt_prefix.py:22)------ 只挑会进 prompt 的字段:

python 复制代码
{
    "name": name,
    "schema": tool["schema"],
    "risky": tool["risky"],
    "description": tool["description"],
}

这解释了为什么"工具 schema 变了"会导致重建:工具说明本来就被渲染进了 prefix 文本 (- read_file(path, start, end) [safe] 读取文件内容)。工具变了 → prefix 文本变了 → hash 变了 → 服务端缓存失效。

9.8 【补充】prefix 里到底装了什么

prefix 可以理解成 agent 的"工作手册" ,四块内容(prompt_prefix.py:build_prompt_prefix()):

块 内容
身份 You are pico, a small local coding agent working inside a local repository.
规则 一次只回一个 <tool> 或 <final>;不要编造工具结果;不要用相同参数重复调同一个工具;必填参数不能为空......
工具清单 每个工具的签名 + 风险标记 + 描述,例如 - read_file(...) [safe] ... / - run_shell(...) [approval required] ...
响应示例 + 工作区快照 <tool>{...}</tool> / <final>...</final> 的样例;以及 Workspace: 段(cwd / repo_root / branch / status / recent_commits / project_docs)

这就把前面几节串起来了:

  • prefix 里含 workspace 段 → 所以工作区变了要重建(第 9.2 节)
  • prefix 里含工具清单 → 所以工具 schema 变了要重建
  • prefix 是"相对稳定的基线" → 所以它适合做缓存和 Prompt Cache 的锚点
  • 而 context_manager.py 的分层预算里,prefix 段拿的是最大的一份(3600 字符,且永不裁到 1200 以下)------ 因为它是最稳定的那部分

十、附:路径符号速查

符号 含义
. 当前目录
.. 上一级目录
../.. 上两级目录
/ 根目录

这几个符号在 Pico 里不只是"读文件时的写法",还是安全边界 :runtime.py:771 的 path() 会把路径 resolve() 之后和 workspace 根做 os.path.commonpath 比对,../ 显式逃逸会被直接拦下(security_event_type=path_escape)。


相关推荐
ShineWinsu1 小时前
对于Redis:AOF持久化的解析
linux·数据库·redis·缓存·面试·持久化·aof
仍然.1 小时前
Redis---集群
数据库·redis·缓存
我命由我123452 小时前
Photoshop - Photoshop 使用仿制图章工具移除对象
学习·职场和发展·产品运营·求职招聘·职场发展·产品经理·学习方法
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
哼べ要伱管2 小时前
26秋季上册-广东省电子基础性作业(小学)
经验分享·学习
JWASX2 小时前
Java 转 go 学习 - 并发编程(2)
学习·golang
~kiss~2 小时前
大模型中的 Q4_K_M 含义
学习
xUxIAOrUIII3 小时前
日常笔记-1005-1
linux·笔记·python·macos