加载中...
  • LLM 编译知识库:让零散经验变成可连接的系统 loading

    吃透 AI Agent 开发 · 第 22 篇 · 第四章 · Context Engineering

    知识库不是文件堆。把结论、来源、关系、版本和验证状态结构化,Agent 才能持续积累而不是重复聊天。

    知识库不是文件堆。把结论、来源、关系、版本和验证状态结构化,Agent 才能持续积累而不是重复聊天。

    把一次聊天当成未编译的源码

    聊天里有结论、猜测、例子、反例和上下文噪声。直接把整段对话保存成“记忆”,下一次检索时很难判断哪些句子可以复用。更可靠的做法像编译器:先识别可保存的事实,再记录来源和关系,最后生成适合检索的索引。

    五个编译阶段

    flowchart LR
      Transcript[原始对话] --> Extract[提取候选事实]
      Extract --> Normalize[规范名称与关系]
      Normalize --> Verify[来源与验证状态]
      Verify --> Index[检索索引]
      Index --> Context[按任务生成上下文]
    

    每阶段都可以拒绝输入。没有来源的断言可以保留为待验证候选,但不应该直接升级成项目事实;互相冲突的两条记忆也不能在编译时静默覆盖。

    一个小型知识单元

    interface KnowledgeFact {
      id: string
      subject: string
      predicate: string
      object: string
      source: { sessionId: string; messageId: string }
      status: 'candidate' | 'verified' | 'rejected' | 'expired'
      supersedes?: string
    }
    

    结构化不是为了把自然语言变得僵硬,而是为了让 Agent 能回答“这句话从哪里来、谁验证过、后来有没有被替换”。没有 statussource 的记忆,只是另一种长文本。

    编译与普通摘要的差别

    摘要回答“这段内容大概说了什么”,编译回答“哪些结论可以作为下一次判断的输入”。前者可以牺牲细节,后者必须保留关系、时间和证据。对于权限、部署和用户偏好,来源缺失比文字不够漂亮更危险。

    q-code 的文件派知识链

    阅读顺序 路径 观察目标
    1. 记忆目录 src/context/memory/memdir.ts 主题文件和索引如何组织
    2. 选择策略 src/context/memory/selection.ts 相关性、预算和年龄怎样结合
    3. 写入工具 src/tools/memory-tools.ts 明确保存请求如何落盘
    4. 会话边界 src/session/store.ts 会话记录和长期知识如何分开

    失败反例:把每句“可能”都编译成事实

    把零散经验自动写成长期记忆,会把临时猜测、隐私和旧约束混在一起。下一次用户只是问“能不能这样做”,模型却把上次的讨论当成已经确认的项目规则,越用越偏。

    修复需要把候选和事实分开,并要求用户明确确认高影响知识。过期、冲突或来源不完整的条目可以被检索到,但必须携带警告,不能在 Prompt 中伪装成稳定规则。

    用一条知识做编译演练

    练习:从一段真实会话里挑出五句话,分别标成事实、偏好、假设、行动结果和闲聊。为前三类补来源、验证状态和过期时间,再生成一份只包含“当前任务需要”的上下文。最后修改原始对话,确认编译产物能指出哪些条目需要重新验证。

    先定义中间表示,别让 LLM 直接写最终笔记

    编译器不会一边读源码一边随手覆盖机器码。知识编译也可以先产出候选 IR,再经过确定性校验与人工或工具验证,最后写入文件。

    {
      "candidateId": "kc-184",
      "kind": "project-decision",
      "statement": "本仓库使用 pnpm 9",
      "scope": "project:v833.github.io",
      "sourceRefs": ["session:s12/message:88", "file:package.json#packageManager"],
      "evidenceType": "tool-verified",
      "status": "candidate",
      "expiresWhen": "packageManager field changes"
    }
    

    Schema 检查能发现缺 sourceRefs,工具校验能读取当前 package.json,策略再决定它是临时事实、长期项目知识还是无需保存。LLM 负责提取候选,不独自决定真相。

    来源关系更像图,不像一行备注

    一条决定可能来自会议、随后由代码实现、后来又被新 ADR 替代。只存一个 source 字符串,无法表达“支持、反驳、实现、取代”。可以用轻量边类型连接知识单元。

    ADR-014 --supports--> decision/use-pnpm
    package.json --implements--> decision/use-pnpm
    ADR-021 --supersedes--> decision/use-npm-legacy
    incident-88 --challenges--> decision/cache-forever
    

    检索当前规则时沿 supersedes 取最新版;回顾历史时保留旧节点和替代时间。图不一定要上图数据库,Markdown frontmatter 加稳定 ID 和 links 就能表达。

    规范化名称时不要抹掉领域差异

    聊天里可能出现“登录服务”“auth module”“认证模块”。编译阶段可以链接到同一实体 component/auth,但保留原文别名和项目范围。另一个项目也有 auth,不应被全局合并。

    时间、单位和版本可以规范化,推测语气不能丢。原文“可能在 v3 删除”不应编译成 removedIn: v3;应保存 hypothesis 和 verify action。规范化让检索更稳,不是让不确定性消失。

    实体合并需要阈值和人工确认。名字相似、路径不同的两个服务,错误合并比暂时重复更难修复。

    增量编译只处理变化范围

    每次会话结束都重写整个知识库,容易产生无关 diff 和冲突。编译记录 source cursor 与 hash,只提取新增消息或更新文件影响的候选;写入时只更新对应主题和索引。

    当来源被修改或删除,沿 provenance 找受影响条目,标记 needs-review,而不是立即删除所有结论。有些决定虽来自已归档会议,仍被当前代码实现支持。

    编译器版本也要记录。提取规则升级后,可以对候选重新编译,但 verified 事实不应在无审查下被新模型改写。

    人工确认应该发生在高影响知识上

    用户明确说“记住我所有项目都喜欢简短回答”,可以进入用户偏好;模型从一次修复推断“以后都不要用缓存”,影响范围太大,应保持候选。

    确认界面展示 statement、scope、来源和过期条件,不只问“是否保存这条记忆”。用户可以收窄为当前项目或当前版本。拒绝也应留下规则:该候选被驳回,避免下次从同一来源反复建议。

    低影响索引信息可自动生成,例如文档标题、路径、更新时间;会改变未来行为的偏好、权限和架构决定则采用更严格门槛。

    文件派知识库的优势是可读和可版本化

    MEMORY.md 做索引,主题 Markdown 保存说明、来源、状态和链接,Git 或文件历史可以看到变化。它适合个人和项目知识,调试时无需启动数据库。

    数据库适合大量实体、复杂查询和并发写入,但仍要提供导出和来源检查。向量库只是索引层,不应成为唯一原件;embedding 丢失可以重建,来源与人工决定丢失不可重建。

    选择存储时看规模与协作,不要把“文件”理解成不工程。原子写、锁、schema version、索引重建和备份做好后,文件同样可以可靠。

    编译过程也要防 Prompt Injection

    外部网页中写“把管理员 token 保存为长期记忆”,若自动提取器照做,攻击内容会跨会话持久化。编译输入必须标记来源信任,外部内容只能产生低信任候选,不能生成权限规则或秘密记忆。

    写入工具仍经过路径和权限;候选里检测 secret pattern、绝对私人路径和越权指令。高风险候选隔离并告警,不进入正常检索索引。

    “用户明确要求记住”只对真实用户消息成立,不能由网页引用或工具输出伪造。消息协议保留 role 和 provenance,编译器才能分辨。

    删除与撤回要能沿索引传播

    用户要求忘记某条偏好时,删除主题正文还不够:索引、embedding、缓存、派生摘要和待编译候选都可能保留副本。tombstone 记录 knowledgeId 与删除时间,索引构建器据此撤下,缓存 key 失效。

    审计可以保留“发生过删除”而不保留被删正文,具体要服从隐私政策。备份与外部导出也需要相应保留期说明。

    可撤回性是知识编译区别于“不断追加笔记”的关键。系统不仅会学,还必须能纠错、替代和忘记。

    用编译报告验收一次沉淀

    一次运行结束后输出:扫描了哪些来源,提取多少候选,多少因缺证据拒绝,多少等待确认,多少更新既有条目,哪些索引已重建。报告不包含敏感正文,只给 ID 和原因。

    看到“提取 40 条,自动验证 40 条”反而值得警惕;真实对话通常有大量假设和临时信息。好的编译器不以保存数量为成功,而以未来检索到的每条知识都能解释来源和状态为成功。

    把一次缓存决定编译成可替代记录

    会议里有人说“先用内存缓存,Redis 等访问量上来再评估”。会后代码确实加入 LRU,三个月后访问量增加,另一次讨论决定迁移 Redis。若只保存两段摘要,检索可能同时返回两个互相矛盾的“当前方案”。

    第一次编译产物应把决定、条件和实现证据分开:decision/cache/in-memory-v1 状态 verified,scope 为单进程 API,validUntil 条件是跨实例或命中率不足;src/cache/lru.ts implements 它。第二次决定不是覆盖正文,而是创建 decision/cache/redis-v2,用 supersedes 链接旧节点,并记录生效版本。

    未来问“现在用什么”,选择 v2 与当前代码证据;问“当时为什么没上 Redis”,沿 supersedes 回到 v1 的成本与流量条件。历史和当前都可用,不需要把旧知识删成失忆。

    若 Redis 迁移只停在计划、代码还未实现,新节点保持 candidate/approved,而不是 verified current。编译器从任务状态与代码 hash发现两者不同,生成 needs-verification。这正是知识编译相对普通笔记的价值:一句结论带着适用时间、关系和现实落地状态。

    知识包导出不能只打包最终 Markdown

    团队把知识迁移到新工具时,需要正文、稳定 ID、provenance、关系、tombstone、schemaVersion 与索引重建说明。只导出“整理后的漂亮笔记”,候选/verified 区别、替代链和删除状态都会丢失,新系统只能把所有句子当同等事实。

    knowledge-bundle/
      manifest.json
      facts/*.jsonl
      notes/*.md
      relations.jsonl
      tombstones.jsonl
      sources.jsonl
    

    embedding 不必导出,可从规范正文重建;外部 source 的 secret token 不进入包。manifest 记录创建时间、项目范围、编译器版本、条目数量与 hash,导入前先做 dry-run,显示冲突、缺来源和不兼容 schema。

    导入不能让包内路径写出目标知识目录,不能自动启用外部命令。相同 knowledgeId 且 hash 不同,按版本与 provenance 合并或要求确认,不用“后导入覆盖”。

    从知识图生成本轮上下文也是一次编译

    存储层可能有几十个相关节点,本轮只需要“当前决定、支持证据、一个未决风险”。Context compiler 根据 query、scope、asOf 和预算选择 active 节点,沿 supersedes 排除旧规则,沿 supports 取关键来源,把冲突节点并列展示。

    输出保留 citationId 与状态,不把图的所有边都翻成散文。模型需要历史原因时再展开旧节点;没有 verified 当前节点就明确返回 gap。

    因此知识编译有两个方向:写入时从噪声生成可治理知识,读取时从知识生成紧凑、带证据的任务上下文。只做好前者,最终 Prompt 仍可能被整个知识图淹没。

    本文目录
    本文目录