ITPub博客

首页 > 数据库 > 国内数据库 > 那条旧规则是从哪来的:给 Agent 的回答补上出处

那条旧规则是从哪来的:给 Agent 的回答补上出处

原创 国内数据库 作者:数据Cool 时间:2026-11-11 13:45:21 0 删除 编辑

我在平台组做数据工程,平时也负责回答业务同事关于数据保留和访问审批的问题。为了少被打扰,我们给内部 Agent 接了一批制度文档,让它先回答一轮。某天一位同事拿着 Agent 的回复来问我:它说某类日志的保留期限是一个数字,可他印象里制度早就改过了。我去查,现行文档里的数字确实不同,Agent 给的是旧版里的数字。

改答案不难,难的是我答不上那个追问:它是从哪看到的?Agent 的回复里写着"根据知识库中的制度文档",这几乎等于没写。我们的文档目录里有现行版本,有一份去年的修订稿,还有一份被人复制到别的空间里的旧版,三份都可能是它的来源。

我先让 Agent 自己交代出处

第一反应是改提示词,要求它回答时列出参考了哪些文档。改完之后每条回复底下都多了一个参考列表,看上去完整多了。

用了几天我就不太信它了。我挑了几条回复对照检索日志,发现它列的文档里有召回过但内容和结论无关的,也有实际起作用的那一份没出现在列表里。这件事让我想明白一个区别:引用关系如果是任务结束后由模型自己总结写回的,记下的是模型认为自己用了什么;运行时在组装上下文那一刻记下的,才是系统实际给了什么。两者最容易不一致的,恰好是那些出了问题、需要复盘的任务。

第二个办法是在我们自建的向量库里给每个切片挂元数据,记下源文件路径。这对直接切分的内容有效,可我们有一部分条目是先从文档里提炼出要点、再合并入库的,合并之后只剩一个笼统的来源字段,分不清哪一句来自哪份文档。一段话原本位于"已废弃方案"一节,切分以后这个标题也不在了。

还有一个更隐蔽的问题。被复制到别的空间里的那份旧版,源头早就有人删过一次,可我们的同步脚本只处理新增和修改,删除事件没有当成正式事件处理,派生出来的切片一直留在库里,照样被召回。来源、作者、时间、权限和版本这几项,从源系统读出来的成本最低,我们当时只读了路径。

同事转来一份帮助文档

后来组里另一位同事转给我一份 Agent Context 的帮助文档,附了一句话:看看 --verbose 那一节。

我只关心一件事:它的检索结果能不能直接回到原始文档。我把那批制度文档上传到一个知识库里试。

ctxdb kb upload-file <kb> <path> --wait

带 --verbose 的检索

文档上传好以后,我拿那位同事的问题去查:

ctxdb memory search "<q>" --knowledge --verbosectxdb kb search "<q>" --kb=<name> --top-k=6 --verbose

加上 --verbose,返回结果里会多出 doc_name、kb_id、doc_id 和 tags。tags 我原本没在意,后来想到,如果每份文档都带着区分"现行"和"修订稿"的标签,检索结果里一眼就能分开,复盘时也不用再打开原文去看封面上的日期。标签该在哪一步、按什么规则打,我还没细看,先记在待办里。这几个字段解决了我最头疼的那个问题:结论来自哪个知识库、哪份文档,可以从字段直接读出来,再用 kb document-get 打开文档,或者用 kb document-chunks 看它被切成了哪些块,确认命中的那段在原文的什么位置。

这和我自己那套元数据的差别在于,来源定位是检索结果的一部分,接入方不用在每个切片上自己维护一份。我顺着 doc_id 回查,才发现知识库里也同时躺着现行版本和那份旧修订稿,两份都会被命中。Agent 并没有凭空编,它拿到的材料本身就新旧并存。

我顺带想了一下这背后的结构。比较清楚的做法是把"定位"和"证据"分层:索引或图谱层回答内容来自哪里、和什么相关,原文层保存未经加工的文件,回答结论对应原文的哪一段。按帮助文档的描述,Agent Context 底层是多模存储,原始文件放在对象存储里,检索结果再通过文档标识指回去。我们自建时也可以用对象存储加一张元数据表搭出类似的结构,只是以前没把"能指回原文"当成硬要求,切分时就把这层关系丢了。

制度文档还算简单。组里有人提过,以后想把培训课件和录屏也放进来,那时候用户会希望结果直接定位到幻灯片的某一页或视频的某个时间点,切分时就得保留页码和时间戳。这一块我还没试,先记下。

实体这一侧也能追。 kb entity-get 返回实体时会带上完整的来源文档,我想知道"日志保留期限"这个对象的信息来自哪几份文档,用它就能看到。我们的制度文档里有不少交叉引用,一条规则的例外情况经常写在另一份文档里,这个能力比我预想的有用。

图谱上下文为什么不能当证据

kb search 有时会附带 graph_context,内容是从图谱里组织出来的关联信息。它带着一个字段 citation_eligible: false,意思是只能帮助理解,不能直接作为引用。

刚看到时我有点不甘心,图谱上下文往往比单个 chunk 读起来完整。仔细想想,我认同这个设计。图谱里的一条关系可能由好几份文档共同支撑,它说"A 和 B 有关",但不对应原文里的某一段。拿它当引用,复盘时又会回到"来自知识库"那种说不清的状态。这和 Agent Context 对知识的整体处理也是一致的:图谱增强检索帮 Agent 找到该看的东西,引用仍然指向原文。我在 Agent 的提示词里明确写了:回答里的依据只能来自 chunk, graph_context 只用来决定下一步去查哪份文档。

需要适应的是,读起来最顺的那部分不能引,Agent 有时会忍不住把它当结论写出来,我得在检查时专门留意。

权限也要跟着走

制度文档里有几份是限定范围可见的,比如涉及审计细节的那几份。我用 kb acl list 看了知识库当前给了哪些主体, kb acl subjects 查可授权的对象, kb acl set 和 kb acl delete 用来调整。

权限这件事我想得比较多。只在入口检查用户能不能打开某个知识库,覆盖不了内容加工之后的流动。一段受限文档被提炼成知识单元,再被组合进任务上下文,权限约束不该在提炼这一步消失。我的原则是派生内容继承来源约束,要扩大可见范围,必须发生一次明确的授权。记忆晋升为知识就是这样一次授权:私有的经验要变成团队共享的知识,得经过评审,由人确认适用范围。

多个来源合并时,最容易出错的是取最宽的权限。一段公开材料和一段受限材料合成一条知识,若按公开处理,受限内容就被稀释出去了。我现在的做法比较保守:受限文档单独放一个知识库,用 acl 控制,不和公开制度混在一起。代价是检索时要多指定一个库,Agent 偶尔会漏查。

合并内容的权限其实有几种处理方式,各有代价。取各来源权限的交集最安全,但合并后的内容可能谁都看不到。拆分知识单元能保留粒度,又背离了合并的初衷。还可以生成一份脱敏版本,那需要额外的规则和审核。我们现在的量还小,分库是最省事的。等受限文档多起来,大概得认真选一种。权限模型也要细到能表达这些决策,至少能区分知识库、成员和用户组,以及读、写、管理几类操作。

血缘里还有一块要我自己补

用了这一圈下来,我把"上下文血缘"分成四个环节来看,看看哪些已经有了着落,哪些还得自己做。

来源有了着落。doc_name、kb_id、doc_id 能回到原文,实体能追到来源文档。来源信息在同步时带上成本最低,等内容被提炼以后再反查,往往只能靠文本相似度去猜。

权限的基本工具也有,粒度够我们现在用,派生内容的权限怎么继承,还得靠我们在放库时守住规矩。

版本我还没摸清。 --verbose 返回的字段里我没看到版本号这一项。现行文档被 kb update-file 替换以后,doc_id 是否保持不变,历史任务引用的那个 doc_id 还能不能还原出当时的内容,我还没测。我的需求是让引用绑定任务当时的版本,展示入口指向最新版本。两者混在一起会造成一种误判:规则更新后,历史任务看上去也引用了新规则,团队于是以为当时的执行违反了规定,实际上执行时生效的是旧版。

如果服务侧给不出版本,我得自己补,选项大概有三种。快照查询快,存储成本高;事件日志写入便宜,要重建某一时刻的状态就得回放;版本链介于两者之间。我们的复盘频率不高,但每次复盘都要求说清楚当时的依据,我倾向于版本链:每次文档替换时记一条"旧 doc 标识、新 doc 标识、替换时间、替换原因",被替代的文档归档保留,不直接删。变化类型也要分开记,原文更新、权限调整和明确停用,在复盘时的含义完全不同。

引用这一环主要得由接入方来做。我把引用分成三类:召回引用表示内容进入了候选集,使用引用表示内容实际进入了上下文,行动引用表示某个外部动作明确依赖这条内容。三者的审计价值差别很大,仅被检索到的旧规则未必影响最终答复,实际驱动了一次审批建议的那段条款,则要记得最完整。召回引用检索服务自己能给出线索,使用引用和行动引用要求我在任务轨迹里写明,存储侧代劳不了。有了这三类,复盘就能逆向走一遍:输出关联了哪些行动引用,行动依据了哪段知识,知识来自哪份文档,这份文档在执行时是否有效、是否可见。

记得越多也未必越安全。审计记录本身可能含有敏感内容,为了可追溯在日志里复制整段原文,等于在权限体系之外又造了一份副本。我现在只记 doc_id、kb_id 和命中的 chunk 位置,不记原文。

保留粒度我按风险分了三档。高风险的写操作和正式对外的答复,保留完整的使用引用和行动引用;日常问答只保留召回引用的摘要;纯闲聊式的提问不记血缘,或者设很短的保留期。分档的依据是复盘时实际会被追问的概率,这个判断目前还是我拍的,没有数据支撑。

回到那条旧规则

那位同事的问题,我最终是这样收尾的:顺着 doc_id 找到旧修订稿,确认它已经失效,用 kb documents-batch-delete 从知识库里移除,被复制出去的那份旧版也一并处理;Agent 再回答同一个问题,依据里出现的是现行文档的 doc_name。我把这次的检索结果和处理过程记在了工单里,以后有人问"它为什么这么说",至少有东西可查。

这件事带给我的感受比较具体:以前排查这类问题,我要在三份文档之间猜;现在第一步就能定位到具体文件,后面的判断还是要人来做,但判断的起点清楚了。这之后我对"Agent 答错了"这个说法更谨慎了一些。链路上任何一处只剩下复制后的文本,团队就分不清错误出在源数据、解析、召回排序、版本选择还是执行逻辑,到头来只能笼统地归给模型。

我给自己定的验收办法

为了不再靠感觉判断"能不能追溯",我定了一个很朴素的抽查流程,每周挑几条 Agent 的回答走一遍:

  • 从回答里的依据找到对应检索结果的 doc_id 和 kb_id。
  • 用 kb document-get 打开文档,确认它是现行版本,而且没有被标记停用。
  • 用 kb document-chunks 找到命中的那一块,看结论能不能被这段原文支撑,限定词有没有被切走。
  • 用 kb acl list 确认提问的人本来就有权看这份文档。

四步里任何一步卡住,就记一条待办。到目前为止卡得最多的是第二步,原因都一样:同一份制度的多个版本同时在库里。这和服务本身关系不大,是我们自己的文档管理欠的账,只是以前没有工具把它暴露出来。

第三步偶尔也会卡。有一次命中的 chunk 里只有条款的后半句,前半句的适用范围被分到了上一块,单看这一块会以为规则对所有日志都适用。原文是对的,切分让它看起来像另一条规则。我现在会把这类文档的关键条款整段写在一节里,减少被切开的机会。

下一步

我打算先改运行时,在组装上下文和发起工具调用的那一刻,把使用引用和行动引用连同 doc_id 写进任务轨迹,不再依赖模型事后总结。召回引用暂时只在调试时记录。轨迹的格式我准备先在一个小的问答场景里跑两周,看复盘时用不用得上,再推到组里其他 Agent。

版本的事还没想清楚。我需要找时间专门测一次 kb update-file 前后 doc_id 和检索结果的变化,再决定是否要在自己的轨迹里额外记文档的修改时间作为版本标识。

轨迹保留多少也没定。高风险的审批类回答要保留完整的引用链,日常问答也许只留采样就够,这条线划在哪里,我想先找合规那边的同事商量过再定。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70049330/viewspace-3188590/ ,如需转载,请注明出处,否则将追究法律责任。

下一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2026-01-19

  • 博文量
    914
  • 访问量
    825272