读到一本悬疑小说的中途,忘了某个配角是谁,随手一搜,第一条结果直接把结局摊在你面前。这个场景,是 LORE 诞生的全部起因。
LORE 是一个开源阅读器,把你手里的书变成一张知识图谱,回答关于这本书的问题,每一句都带出处,而且绝不越过你当前读到的章节。作者把它的工作原理和踩过的坑写了出来。
为什么不直接问通用聊天机器人?
拿一本名著去问通用聊天机器人,它会凭记忆回答。这条路在三个地方断掉:
- 它给不出引用。你拿到一段自信满满的文字,没有任何办法核对。
- 它不认识你这本书。冷门小说、新书、你自己手上的版本,它都在猜。
- 它不知道你读到哪。你在第 4 章问"这个角色可信吗",它会兴高采烈地讲第 30 章的事。
所以 LORE 的规则定死了:只从文本里回答,每句都引用,绝不越过读者的阅读位置。
一次读入,之后都是查图
上传 EPUB 或 PDF 后,LORE 只通读一遍:先按章节和段落切分,书里没有干净标题时改用目录;再用大模型抽取人物、地点、群体、物品、事件以及它们之间的关系,模型密钥自带,Gemini、Claude 或 OpenAI 都行。
接着做实体消解。"Holmes""Mr. Sherlock Holmes""the detective"会被合并成同一个节点;角色隐藏身份这类设定,会变成显式的 IDENTIFIED_AS 链接。图谱存进 Kuzu,段落向量存进 LanceDB,向量用 Ollama 上的 nomic-embed-text 在本地算。
提问时的流程是:把问题拆成需要分别回答的部分,用向量搜索加图谱找到起始实体,沿图谱走出相关关系,取回这些关系背后的原始段落,抽取带原文引句的论断,逐条核对论断与引句,砍掉对不上的,最后写成散文,每句都有引用。
图谱是多书提问的前提。"这个角色在整个系列里怎么变化"是在一个实体的关系上按故事顺序走一遍,而不是指望正确的段落恰好挤进向量搜索的前十名。
每句话都得挣到自己的位置
核心数据结构故意做得很朴素:一条论断包含文本、它依据的段落编号、以及该段落里的原话。没有引句的论断,或者引句并不在它所引段落里的论断,永远到不了读者面前。之后还有第二轮检查,确认段落确实支撑这条论断,而不只是引句存在。活下来的才会被写成文章。
把检查器做对本身就是一个项目。出过的问题包括:
- 判决反复横跳。第一轮接受的论断,在同一答案的第二轮被拒。修法是后续轮次沿用第一轮的判决。
- 自相矛盾。检查器写出的理由支持这条论断,却仍然判它不通过。修法是先要理由再要判决,凡是理由与判决打架的驳回,重新问一遍。
- 搞错谁对谁做了什么。写作环节把"A 指控 B"写成"B 指控 A",引用却原样保留。修法是在写作和两道检查里都写明谁对谁做了什么。
每个修复都挂在开关后面,每个开关都配一组盲测对照:同样的问题分别在开关打开和关闭时回答,评分时不知道哪份是哪份,再用符号检验比较。在留出问题上赢不了的改动,不上线。作者顺带吐槽:想在一个 RAG 系统上"改进"你反复手测的那五个问题,实在太容易了。
防剧透是过滤器,不是提示词
最诱人的做法是写一句提示词:"不要透露第 4 章之后的任何内容。"这不管用。只要证据进了上下文,它就会漏出来。
所以 LORE 干脆不检索它。你的阅读进度变成每本书的章节上限,这个上限作为前置过滤条件,作用在每一次向量搜索、每一次图谱查询和每一份角色档案上。简化后的写法是:给每本书记下读者已到达的章节序号,查询时只放行未超限的段落。
作者还记录了一个前端层面的坑:按钮在"停止"和"提问"两种状态间切换时,React 可能复用同一个元素,导致状态错乱。修法是让两种状态渲染成两个不同的元素。麻烦之处在于,测试里的合成点击复现不了这个问题,因为触发重渲染的微任务在整个事件派发之后才跑。于是测试改为检查结构:点过"停止"之后,"提问"按钮必须是另一个元素。
LORE 还暴露了一个带 OAuth 的 MCP 服务,可以把你的书库作为自定义连接器加进 Claude,在 Claude 里直接问书里的问题,引用一样,阅读位置一样生效。
技术栈方面:后端是 Python、FastAPI、Kuzu 图数据库、LanceDB 向量库、Postgres 存账号、Ollama 算向量;前端是 React 加 TypeScript,图谱会随着答案的检索过程动起来;部署是单台 ARM 虚拟机上的 Docker Compose,前面挂 Cloudflare Tunnel。
作者自己说,这东西有些地方还很粗糙:答案的质量取决于抽取的质量,长系列处理起来要等很久。他最想要的反馈,恰恰是它答错的例子。
热门跟贴