本文面向想了解 ChatCrystal 如何自动发现笔记间关联的开发者。预计阅读时间10 分钟什么是「知识图谱」在 ChatCrystal 的语境下知识图谱不是 Neo4j 那种企业级图数据库而是一张笔记关系网络节点是你的笔记边是笔记之间的语义关系。每条边带三个属性——关系类型比如「被解决」、置信度0-1、来源LLM 自动发现或手动创建。存储层很简单一张note_relations表就够了CREATETABLEnote_relations(idINTEGERPRIMARYKEYAUTOINCREMENT,source_note_idINTEGERNOTNULL,target_note_idINTEGERNOTNULL,relation_typeTEXTNOTNULL,confidenceREALDEFAULT1.0,descriptionTEXT,created_byTEXTNOTNULLDEFAULTmanual,-- manual 或 llmcreated_atTEXTDEFAULT(datetime(now)),UNIQUE(source_note_id,target_note_id,relation_type),FOREIGNKEY(source_note_id)REFERENCESnotes(id)ONDELETECASCADE,FOREIGNKEY(target_note_id)REFERENCESnotes(id)ONDELETECASCADE);注意UNIQUE约束同一对笔记、同一种关系类型只能存在一条记录。created_by字段区分来源——manual是用户手动创建的llm是自动发现的。八种关系类型ChatCrystal 定义了 8 种关系类型覆盖了日常开发中笔记之间最常见的语义联系类型含义例子CAUSED_BYA 的问题由 B 引起「CORS 报错」← 由 ← 「Nginx 反代配置修改」LEADS_TOA 导致了 B 的结果「升级 Node 20」→ 导致 → 「ESM 模块加载失败」RESOLVED_BYA 的问题被 B 解决「SQLite 内存溢出」→ 被解决 → 「切换 sql.js WASM」SIMILAR_TO主题或技术相似「React 状态管理方案对比」≈「Vue Pinia 选型分析」CONTRADICTS结论互相矛盾「JWT 无状态优于 Session」╳「Session 更安全」DEPENDS_ONA 依赖于 B「GraphQL 分页实现」← 依赖 ←「Apollo Client 缓存策略」EXTENDSA 是 B 的扩展「Tailwind v4 迁移踩坑」← 扩展 ←「Tailwind v3 自定义主题」REFERENCESA 引用了 B 的内容「CI 流水线优化」← 引用 ←「GitHub Actions 缓存机制」这 8 种类型由 Zod schema 约束LLM 只能从这个枚举里选择不会自由发挥。自动发现LLM 如何分析笔记关系这是整个系统的核心。当你生成一条新笔记时ChatCrystal 可以自动发现它与已有笔记之间的关系。整个流程分三步。第一步候选笔记筛选discoverRelations(noteId)首先获取源笔记的完整信息——标题、摘要、关键结论、标签然后去数据库里找「可能有关系」的候选笔记asyncfunctionfindCandidateNotes(noteId:number,noteTitle:string):PromiseCandidateNote[]{// 优先用语义搜索找相似笔记letcandidateIds:number[][];try{constsearchResultsawaitsemanticSearch(noteTitle,MAX_CANDIDATES);candidateIdssearchResults.map(rr.noteId).filter(idid!noteId);}catch{// 语义搜索不可用走降级逻辑}if(candidateIds.length0){// 按语义相似度选出来的候选// ...}// 降级取最近的 20 条笔记// ...}这里有两个关键设计语义搜索优先用 Embedding 模型把笔记标题向量化在 vectra 索引里找到最相似的 20 条。这意味着如果两条笔记讨论的是同一个技术问题即使措辞完全不同也能被选为候选。降级兜底如果 Embedding 模型没配置、索引为空就退化为按时间倒序取最近 20 条。覆盖率低但不会报错。第二步LLM 分析拿到候选列表后系统构造一个结构化 prompt 发给 LLM。prompt 包含源笔记的详细信息和候选笔记的摘要列表每条候选标注了 ID、标题、摘要截取前 200 字和标签新笔记 标题: SQLite WASM 内存优化方案 摘要: 在处理大规模数据导入时发现 sql.js 的内存占用过高... 标签: sqlite, wasm, performance 已有笔记 [id42] Nginx 反代导致 WebSocket 断连 - 生产环境出现频繁断连... [nginx, websocket] [id43] 数据导入管道重构 - 将批量插入改为分批事务... [sqlite, batch]LLM 使用 Vercel AI SDK 的generateObject()返回结构化 JSON 数组每条关系包含target_note_id、relation_type、confidence0-1和description20 字以内。系统 prompt 明确要求只返回置信度 0.5 的关系最多返回 5 条不要编造不存在的关系第三步过滤与持久化LLM 的输出不是直接入库还要过两道过滤候选 ID 验证target_note_id必须在候选集合里——防止 LLM 幻觉出一个不存在的笔记 ID置信度阈值confidence 0.5的才保留最多 5 条通过过滤的关系以INSERT OR IGNORE写入数据库。OR IGNORE处理 UNIQUE 约束冲突——如果同一条关系已经存在跳过而不是报错。写入后立即saveDatabase()持久化到磁盘。constfilteredRelationsrawRelations.filter(relcandidateIdSet.has(rel.target_note_id)rel.confidenceMIN_CONFIDENCE).slice(0,MAX_RELATIONS);手动创建关系除了 LLM 自动发现你也可以通过 API 手动创建关系。这在 LLM 漏掉了一些你认为重要的关联时很有用curl-XPOST http://localhost:3721/api/notes/42/relations\-HContent-Type: application/json\-d{ target_note_id: 58, relation_type: RESOLVED_BY, description: 被新的 WASM 方案解决 }手动创建的关系confidence默认为 1.0created_by为manual。API 会验证两端笔记都存在且不允许自引用source_note_id target_note_id。删除关系同样简单curl-XDELETE http://localhost:3721/api/relations/7图数据 APInodes edges知识图谱的可视化需要一个专门的图数据接口。GET /api/relations/graph返回完整的节点和边集合# 获取全量图数据curlhttp://localhost:3721/api/relations/graph# 按项目过滤curlhttp://localhost:3721/api/relations/graph?projectChatCrystal返回格式{success:true,data:{nodes:[{id:42,title:SQLite WASM 内存优化方案,project_name:ChatCrystal,tags:[sqlite,wasm,performance]}],edges:[{source:42,target:58,type:RESOLVED_BY,confidence:0.85}]}}节点数据来自notes和conversations表的 JOIN 查询通过子查询聚合标签。边数据从note_relations表取出后会过滤掉两端节点不在当前视图中的边——如果你按项目过滤了节点孤立的边不会出现在结果里。关系扩展搜索这是知识图谱最实用的功能之一。语义搜索只能找到与查询词直接匹配的笔记但很多时候你需要的是「关联知识」。比如你搜「SQLite 内存问题」直接命中的可能是「sql.js WASM 内存优化」但沿着图谱边走下去你可能还会发现「数据导入管道重构」——它通过EXTENDS关系连接到内存优化笔记。当搜索请求带expandRelationstrue参数时ChatCrystal 会沿着图谱边扩展搜索结果从直接命中的笔记出发遍历所有关联边只扩展置信度 0.5 的边关联笔记的得分 原始得分 x 0.7 x 边置信度0.7 的折扣系数是关键它确保关联笔记永远排在直接命中之后但又不至于被完全忽略。这个设计让搜索结果既精准又有广度。批量发现如果你的笔记库里已经有大量笔记但还没有关系可以一键触发批量发现curl-XPOST http://localhost:3721/api/relations/batch-discover这个接口会找出所有「没有作为任何关系的 source」的笔记为它们逐个排队触发 LLM 发现。任务进入 p-queue 队列并发度 1每秒 1 次请求不会打爆你的 LLM API。返回值告诉你排队了多少任务{success:true,data:{queued:15,queue:{pending:3,running:1}}}批量发现的筛选逻辑很精确——只处理完全没有出边的笔记不会重复处理已有关系的笔记。如果你想重新发现某个笔记的关系可以先删除旧关系再单独触发。可视化Web 界面的知识图谱页面消费的就是GET /api/relations/graph返回的 nodes edges 数据。前端用力导向图force-directed layout渲染节点大小反映关系数量边的颜色区分关系类型悬停显示置信度和描述。你可以通过项目过滤器切换视图——只看某个项目的知识图谱避免节点过多导致图谱混乱。下一步知识图谱系统目前的几个已知方向双向关系当前边是有方向的source - target但某些关系类型如SIMILAR_TO天然是双向的。未来可以考虑在查询时自动对称展开。关系强度衰减随着时间推移旧的关系可能不再重要。可以引入时间衰减因子让近期的关系在搜索扩展中权重更高。图谱分析基于图结构做一些简单的分析——比如找出「枢纽笔记」关系最多的节点、发现「知识孤岛」没有任何关系的笔记。增量发现当前批量发现是全量扫描未来可以在每次导入新对话后自动触发增量发现只处理新增笔记。知识图谱把 ChatCrystal 从一个「笔记搜索工具」提升为「知识网络」。当你积累了足够的笔记和关系它就能帮你发现那些你自己都忘了的关联——这恰恰是 AI 对话知识管理最有价值的地方。项目地址github.com/ZengLiangYi/ChatCrystal
知识图谱:笔记关系发现与可视化
本文面向想了解 ChatCrystal 如何自动发现笔记间关联的开发者。预计阅读时间10 分钟什么是「知识图谱」在 ChatCrystal 的语境下知识图谱不是 Neo4j 那种企业级图数据库而是一张笔记关系网络节点是你的笔记边是笔记之间的语义关系。每条边带三个属性——关系类型比如「被解决」、置信度0-1、来源LLM 自动发现或手动创建。存储层很简单一张note_relations表就够了CREATETABLEnote_relations(idINTEGERPRIMARYKEYAUTOINCREMENT,source_note_idINTEGERNOTNULL,target_note_idINTEGERNOTNULL,relation_typeTEXTNOTNULL,confidenceREALDEFAULT1.0,descriptionTEXT,created_byTEXTNOTNULLDEFAULTmanual,-- manual 或 llmcreated_atTEXTDEFAULT(datetime(now)),UNIQUE(source_note_id,target_note_id,relation_type),FOREIGNKEY(source_note_id)REFERENCESnotes(id)ONDELETECASCADE,FOREIGNKEY(target_note_id)REFERENCESnotes(id)ONDELETECASCADE);注意UNIQUE约束同一对笔记、同一种关系类型只能存在一条记录。created_by字段区分来源——manual是用户手动创建的llm是自动发现的。八种关系类型ChatCrystal 定义了 8 种关系类型覆盖了日常开发中笔记之间最常见的语义联系类型含义例子CAUSED_BYA 的问题由 B 引起「CORS 报错」← 由 ← 「Nginx 反代配置修改」LEADS_TOA 导致了 B 的结果「升级 Node 20」→ 导致 → 「ESM 模块加载失败」RESOLVED_BYA 的问题被 B 解决「SQLite 内存溢出」→ 被解决 → 「切换 sql.js WASM」SIMILAR_TO主题或技术相似「React 状态管理方案对比」≈「Vue Pinia 选型分析」CONTRADICTS结论互相矛盾「JWT 无状态优于 Session」╳「Session 更安全」DEPENDS_ONA 依赖于 B「GraphQL 分页实现」← 依赖 ←「Apollo Client 缓存策略」EXTENDSA 是 B 的扩展「Tailwind v4 迁移踩坑」← 扩展 ←「Tailwind v3 自定义主题」REFERENCESA 引用了 B 的内容「CI 流水线优化」← 引用 ←「GitHub Actions 缓存机制」这 8 种类型由 Zod schema 约束LLM 只能从这个枚举里选择不会自由发挥。自动发现LLM 如何分析笔记关系这是整个系统的核心。当你生成一条新笔记时ChatCrystal 可以自动发现它与已有笔记之间的关系。整个流程分三步。第一步候选笔记筛选discoverRelations(noteId)首先获取源笔记的完整信息——标题、摘要、关键结论、标签然后去数据库里找「可能有关系」的候选笔记asyncfunctionfindCandidateNotes(noteId:number,noteTitle:string):PromiseCandidateNote[]{// 优先用语义搜索找相似笔记letcandidateIds:number[][];try{constsearchResultsawaitsemanticSearch(noteTitle,MAX_CANDIDATES);candidateIdssearchResults.map(rr.noteId).filter(idid!noteId);}catch{// 语义搜索不可用走降级逻辑}if(candidateIds.length0){// 按语义相似度选出来的候选// ...}// 降级取最近的 20 条笔记// ...}这里有两个关键设计语义搜索优先用 Embedding 模型把笔记标题向量化在 vectra 索引里找到最相似的 20 条。这意味着如果两条笔记讨论的是同一个技术问题即使措辞完全不同也能被选为候选。降级兜底如果 Embedding 模型没配置、索引为空就退化为按时间倒序取最近 20 条。覆盖率低但不会报错。第二步LLM 分析拿到候选列表后系统构造一个结构化 prompt 发给 LLM。prompt 包含源笔记的详细信息和候选笔记的摘要列表每条候选标注了 ID、标题、摘要截取前 200 字和标签新笔记 标题: SQLite WASM 内存优化方案 摘要: 在处理大规模数据导入时发现 sql.js 的内存占用过高... 标签: sqlite, wasm, performance 已有笔记 [id42] Nginx 反代导致 WebSocket 断连 - 生产环境出现频繁断连... [nginx, websocket] [id43] 数据导入管道重构 - 将批量插入改为分批事务... [sqlite, batch]LLM 使用 Vercel AI SDK 的generateObject()返回结构化 JSON 数组每条关系包含target_note_id、relation_type、confidence0-1和description20 字以内。系统 prompt 明确要求只返回置信度 0.5 的关系最多返回 5 条不要编造不存在的关系第三步过滤与持久化LLM 的输出不是直接入库还要过两道过滤候选 ID 验证target_note_id必须在候选集合里——防止 LLM 幻觉出一个不存在的笔记 ID置信度阈值confidence 0.5的才保留最多 5 条通过过滤的关系以INSERT OR IGNORE写入数据库。OR IGNORE处理 UNIQUE 约束冲突——如果同一条关系已经存在跳过而不是报错。写入后立即saveDatabase()持久化到磁盘。constfilteredRelationsrawRelations.filter(relcandidateIdSet.has(rel.target_note_id)rel.confidenceMIN_CONFIDENCE).slice(0,MAX_RELATIONS);手动创建关系除了 LLM 自动发现你也可以通过 API 手动创建关系。这在 LLM 漏掉了一些你认为重要的关联时很有用curl-XPOST http://localhost:3721/api/notes/42/relations\-HContent-Type: application/json\-d{ target_note_id: 58, relation_type: RESOLVED_BY, description: 被新的 WASM 方案解决 }手动创建的关系confidence默认为 1.0created_by为manual。API 会验证两端笔记都存在且不允许自引用source_note_id target_note_id。删除关系同样简单curl-XDELETE http://localhost:3721/api/relations/7图数据 APInodes edges知识图谱的可视化需要一个专门的图数据接口。GET /api/relations/graph返回完整的节点和边集合# 获取全量图数据curlhttp://localhost:3721/api/relations/graph# 按项目过滤curlhttp://localhost:3721/api/relations/graph?projectChatCrystal返回格式{success:true,data:{nodes:[{id:42,title:SQLite WASM 内存优化方案,project_name:ChatCrystal,tags:[sqlite,wasm,performance]}],edges:[{source:42,target:58,type:RESOLVED_BY,confidence:0.85}]}}节点数据来自notes和conversations表的 JOIN 查询通过子查询聚合标签。边数据从note_relations表取出后会过滤掉两端节点不在当前视图中的边——如果你按项目过滤了节点孤立的边不会出现在结果里。关系扩展搜索这是知识图谱最实用的功能之一。语义搜索只能找到与查询词直接匹配的笔记但很多时候你需要的是「关联知识」。比如你搜「SQLite 内存问题」直接命中的可能是「sql.js WASM 内存优化」但沿着图谱边走下去你可能还会发现「数据导入管道重构」——它通过EXTENDS关系连接到内存优化笔记。当搜索请求带expandRelationstrue参数时ChatCrystal 会沿着图谱边扩展搜索结果从直接命中的笔记出发遍历所有关联边只扩展置信度 0.5 的边关联笔记的得分 原始得分 x 0.7 x 边置信度0.7 的折扣系数是关键它确保关联笔记永远排在直接命中之后但又不至于被完全忽略。这个设计让搜索结果既精准又有广度。批量发现如果你的笔记库里已经有大量笔记但还没有关系可以一键触发批量发现curl-XPOST http://localhost:3721/api/relations/batch-discover这个接口会找出所有「没有作为任何关系的 source」的笔记为它们逐个排队触发 LLM 发现。任务进入 p-queue 队列并发度 1每秒 1 次请求不会打爆你的 LLM API。返回值告诉你排队了多少任务{success:true,data:{queued:15,queue:{pending:3,running:1}}}批量发现的筛选逻辑很精确——只处理完全没有出边的笔记不会重复处理已有关系的笔记。如果你想重新发现某个笔记的关系可以先删除旧关系再单独触发。可视化Web 界面的知识图谱页面消费的就是GET /api/relations/graph返回的 nodes edges 数据。前端用力导向图force-directed layout渲染节点大小反映关系数量边的颜色区分关系类型悬停显示置信度和描述。你可以通过项目过滤器切换视图——只看某个项目的知识图谱避免节点过多导致图谱混乱。下一步知识图谱系统目前的几个已知方向双向关系当前边是有方向的source - target但某些关系类型如SIMILAR_TO天然是双向的。未来可以考虑在查询时自动对称展开。关系强度衰减随着时间推移旧的关系可能不再重要。可以引入时间衰减因子让近期的关系在搜索扩展中权重更高。图谱分析基于图结构做一些简单的分析——比如找出「枢纽笔记」关系最多的节点、发现「知识孤岛」没有任何关系的笔记。增量发现当前批量发现是全量扫描未来可以在每次导入新对话后自动触发增量发现只处理新增笔记。知识图谱把 ChatCrystal 从一个「笔记搜索工具」提升为「知识网络」。当你积累了足够的笔记和关系它就能帮你发现那些你自己都忘了的关联——这恰恰是 AI 对话知识管理最有价值的地方。项目地址github.com/ZengLiangYi/ChatCrystal