把笔记画成会漂浮的节点,确实容易让知识库显得聪明。但两张卡片挨得近,不代表它们互相支持;一条力导向连线,也不会自动成为引用依据。若这张图后面接 AI 回答,我会先解决资料从哪里来的问题。
图谱只负责可见关系
这个小例子使用人工标签,不计算向量相似度。查询与笔记共享多少标签,就给多少分;只拿前两条作为上下文,并把稳定 ID 放在每段原文前。标签图适合浏览,检索排序适合筛选,回答里的事实则仍需要对应来源。
OpenAI 在 10 月 22 日介绍的 Decisions API展示了有界任务的方向。这里没有调用它,也不把标签匹配包装成语义理解;我希望先让确定性的选择过程可回放,再考虑模型是否能改善表达。
引用 ID 也需要验收
下面的检查器只保证回答标出的 ID 来自选中的上下文。它不能判断一句话是否真的被原文支持:模型完全可能引用 A,却写出 A 没有说过的结论。因此产品界面应允许展开引用片段,把原文和推断分开,并为没有资料的问题保留“无法回答”。
输入笔记也可能包含指令式文字。被检索出来的内容是资料,不是运行命令的授权;不要把其中的“忽略前文”“下载脚本”交给执行器。本例只拼接文本,不执行任何资料内容。
运行node context.js,可验证排序稳定、无命中返回空、未知引用被拒绝。数据为教学笔记,没有真实模型、向量数据库或浏览器图形测试。若以后加入向量召回,应继续保留检索 ID、截取规则和原文,而不是只保存一张漂亮的关系图。
可独立运行的核心例子
const notes=[ {id:'A',tags:['export','svg'],text:'导出与预览应使用同一份SVG。'}, {id:'B',tags:['export','backup'],text:'用户应能下载可迁移的备份。'}, {id:'C',tags:['color'],text:'配色命名是提案,不是像素统计。'} ]; function retrieve(notes,tags,limit=2) { if(!Number.isInteger(limit)||limit<1)throw Error('limit无效'); const query=new Set(tags),ids=new Set(); return notes.map((n,index)=>{ if(!/^[A-Z0-9_-]+$/.test(n.id)||ids.has(n.id))throw Error('ID重复或不合法'); ids.add(n.id);return {...n,index,score:[...new Set(n.tags)].filter(t=>query.has(t)).length}; }).filter(n=>n.score>0).sort((a,b)=>b.score-a.score||a.index-b.index).slice(0,limit); } function context(rows){return rows.map(n=>`[source:${n.id}]\n${n.text}`).join('\n\n');} function checkCitations(answer,rows) { const allowed=new Set(rows.map(n=>n.id)); const cited=[...answer.matchAll(/\[source:([A-Z0-9_-]+)\]/g)].map(m=>m[1]); if(!cited.length||cited.some(id=>!allowed.has(id)))throw Error('引用未在上下文中'); return {ids:cited,semanticSupport:'仍需逐句人工检查'}; } const selected=retrieve(notes,['export']); if(selected.map(n=>n.id).join()!=='A,B'||retrieve(notes,['unknown']).length)throw Error('召回错误'); let rejected=false;try{checkCitations('结论[source:C]',selected);}catch{rejected=true;} if(!rejected)throw Error('不应接受未选来源'); console.log(context(selected));console.log(checkCitations('导出共用数据[source:A]',selected));