构建数据库驱动的 LLM Wiki
本挑战是构建你自己的数据库驱动的 LLM Wiki。
AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | PLC在线仿真器 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo
本挑战是构建你自己的个人知识库工具——一个由 LLM Agent 阅读你精心整理的资料源、提取关键信息,并为你构建一个不断增长的、相互链接的活态 Wiki 的系统。
大多数人对 LLM 和文档的体验是 RAG:上传文件、提出问题、从检索到的片段中拼接答案。这可以工作,但 LLM 每次都从头重新发现知识。问一个横跨五篇文档的微妙问题,系统就必须找到并拼凑它之前见过的片段。没有任何积累。
LLM Wiki 采用了不同的方法。与其在查询时从原始文档中检索,LLM Agent 增量地构建和维护一个持久的 Wiki——一组结构化的 Markdown 文件,位于你和你的资料源之间。当你添加新的资料源时,Agent 阅读它,提取关键信息,并将其整合到现有 Wiki 中:更新实体页面、修订主题摘要、标注新数据与旧声明的矛盾之处、加强或挑战不断演进的综合性结论。知识只编译一次,然后保持更新,而不是在每次查询时重新推导。
你负责资料获取、探索和提出好问题。LLM 负责所有繁重工作:摘要、交叉引用、归档,以及让知识库随时间真正有用的各种管理工作。
本挑战的灵感来自 Andrej Karpathy 的 LLM Wiki 概念——一种使用 LLM 构建个人知识库的模式。Karpathy 在抽象层面描述了这个想法;我们的任务是构建一个可运行的实现。
在底层,系统将每个 Wiki 页面的向量嵌入和全文索引存储在 Oracle AI Database 中。当你提出问题时,它使用混合搜索找到相关页面,LLM 合成带有引用的答案。Agent 行为——数据摄取工作流、多步查询、lint 检查——使用 LangGraph 的状态机模型进行编排,而 LangChain 处理 LLM 集成。这是一个关于基于 Agent 的知识管理、向量搜索、全文搜索以及构建随时间真正增值的工具的实践入门。
1、挑战 - 构建 LLM Wiki
你将构建一个由 LLM Agent 为你编写和维护的个人知识库。它首先将资料源文档摄取到一组 Markdown 文件的 Wiki 中,然后让你通过 Web 界面或 CLI 聊天来查询它。逐步地,你将添加 Wiki 脚手架、资料摄取、索引和日志管理、向量存储、语义检索、项目管理、lint 检查和用户界面。最终,你将拥有一个真正帮助你构建和导航不断增长的知识体的工具。
2、环境设置
在这一入门步骤中,你将设置好环境,准备开始开发和测试你的解决方案。
你需要做出一些决策并搭建一些基础设施:
1)设置你的向量和全文数据库。 你需要在本地 Docker 容器中运行 Oracle Database 26ai。拉取 container-registry.oracle.com/database/free:latest 镜像,启动容器,并为管理员账户设置密码。你可以在 Oracle Database Free 入门指南 中找到完整的设置说明。容器运行后,使用 SQL 客户端连接并验证你可以创建表。将所有凭据存储在环境文件中,不要硬编码在任何地方。
docker pull container-registry.oracle.com/database/free:latest
docker run -d -p 1521:1521 -e ORACLE_PWD=<your-password> container-registry.oracle.com/database/free:latest
2)选择你的嵌入模型。 你需要一个能捕获语义含义的文本嵌入模型。Nomic 的 nomic-embed-text 是开源的(Apache 2.0),可以在本地 CPU 上运行。它生成 768 维向量。通过 Hugging Face 安装:pip install sentence-transformers,并加载为 nomic-ai/nomic-embed-text-v2-moe。任何具有合理语义质量的通用嵌入模型都可以——关键要求是它能足够好地捕获 Wiki 页面的含义,以便为给定查询找到相关页面。
3)设置你的 LLM 提供商。 你需要一个语言模型来编写 Wiki 页面、摘要资料源和回答问题。任何提供聊天 API 的提供商都可以——Anthropic、OpenAI、Google、Mistral,或通过 Ollama 运行的本地模型。模型需要足够强大,能够编写连贯的 Markdown 并从资料源文档中提取结构化信息。
4)设置 LangChain 和 LangGraph。 你将使用 LangChain 进行 LLM 集成,使用 LangGraph 编排 Agent 的工作流。安装两者:pip install langchain langgraph。LangChain 处理提示和响应解析的管道工作。LangGraph 处理 Agent 流程——摄取资料源的多步过程(读取、提取、编写摘要、更新页面、更新索引、日志记录)、回答查询(搜索、读取页面、合成)和运行 lint 检查。
5)选择你的第一个 Wiki 主题。 选择一个你真正感兴趣的领域,收集 50-100 篇资料源文档——文章、论文、博客帖子——关于它的内容。将它们保存为 Markdown 或纯文本文件。这将是你测试用的语料库。选择那些文章自然会引用相同实体和概念的主题,这样你的 Wiki 就有可以发现和呈现的关联。技术主题效果很好(例如数据库内部原理、特定的 ML 技术、一种编程语言),但任何有深度的领域都可以——历史、烹饪、健身,任何你感兴趣的领域。
测试: 验证你的 Oracle Database 容器正在运行并且你可以连接到它。加载你的嵌入模型并生成一个测试嵌入,确认它返回一个 768 维的向量。对你的 LLM API 进行一次测试调用,确认它返回有效响应。验证你的环境文件被正确读取,源代码中没有凭据。
3、构建Wiki脚手架
在这一步中,你的目标是构建 Wiki 脚手架——创建和管理将成为你知识库的 Markdown 文件目录的系统。
Wiki 以 Markdown 文件目录的形式存在于磁盘上。在你摄取资料源或回答问题之前,你需要创建页面、向它们写入内容并将它们链接在一起的基础设施。可以把这看作知识库的文件系统层。
首先定义一个新的 Wiki 在磁盘上的样子。当你创建一个 Wiki 时,系统应该搭建一个包含不同页面类型子目录的目录结构:summaries 用于资料源摘要,entities 用于关于人物/公司/概念的页面,topics 用于概览页面,以及 raw 目录用于原始资料源文件。在目录之外,创建一个 schema 文件(命名为 SCHEMA.md),定义这个 Wiki 的约定——目录的用途、使用什么命名约定、页面应该有什么 frontmatter 字段,以及交叉引用应该如何格式化。
页面顶部应包含 YAML frontmatter 元数据:标题、类型(entity、concept、summary、overview)、创建日期、更新日期、标签,以及页面引用的资料源列表。frontmatter 使页面以后可查询,也让 Obsidian 的 Dataview 插件等工具能生成动态视图。
页面之间的交叉引用应使用标准 Markdown 链接或 wikilinks([[Page Name]])。使用哪种风格由你决定,但 schema 文件应记录约定,以便 Agent 保持一致。当一个页面引用另一个页面时,Agent 应该能够跟随该链接、读取目标页面并更新关系的双方。
构建一个简单的 CLI,让你可以创建新的 Wiki、列出现有 Wiki 并检查 Wiki 的结构——它有多少页面、存在哪些目录、schema 说了什么。这个 CLI 只用于开发和测试;你稍后会用一个正式的界面替换它。
测试:
- 创建一个 Wiki。验证目录结构和 SCHEMA.md 文件已在磁盘上创建。
- 手动添加几个带有 frontmatter 和交叉引用的测试页面。验证 frontmatter 正确解析,链接指向预期路径。
- 使用不同的 schema 配置(不同的 frontmatter 字段、不同的链接风格)创建第二个 Wiki。验证两个 Wiki 共存并遵循各自的约定。
- 列出你的 Wiki 并验证两者都出现。
- 手动删除一个 Wiki 目录并验证列表正确反映其不存在。
4、构建资料摄取管道
在这一步中,你的目标是构建资料摄取管道——读取资料源文档并将其知识整合到 Wiki 中的 Agent 工作流。
这是系统的核心。当你将新的资料源放入 raw 目录并告诉 Agent 摄取它时,一个多步工作流就开始了。Agent 读取资料源,提取关键信息,(在交互模式下)与你讨论要点,然后更新多个 Wiki 页面。
将摄取工作流建模为 LangGraph 中的图。每个节点处理一个关注点:读取资料源、提取实体和概念、识别声明和关键信息、在 summaries 目录中编写摘要页面、为每个发现的实体更新或创建实体页面、为每个概念更新或创建概念页面、修订主题概览页面、标记与现有内容的矛盾、更新索引,以及向日志追加条目。单个资料源可能会涉及 10-15 个 Wiki 页面。
Agent 应该能够检测矛盾。当新资料源做出与 Wiki 中已有内容冲突的声明时,Agent 应在相关页面上注明差异,而不是静默覆盖或忽略它。用户应该能看到资料源之间的分歧并做出自己的判断。
Agent 还应识别空白——资料源中引用但尚无页面的实体或概念——并创建存根页面或标记以供后续关注。
仔细思考你如何为每个任务提示 LLM。Wiki 的质量完全取决于提取和合成的质量。你可能需要针对不同页面类型使用不同的提示:摘要页面提示、实体页面提示、概念页面提示等。你在 Step 1 中构建的 schema 文件应指导这些提示。
原始资料源文件应放入 raw 目录且永远不被修改。Agent 从中读取但从不写入。这是你的真相来源。
测试:
- 将单个资料源文档(一篇短文,500-1000 字)摄取到测试 Wiki 中。验证 Agent 创建了一个捕获关键点且没有捏造资料源中不存在的事实的摘要页面。
- 验证 Agent 为资料源中提及的关键人物、公司或概念创建或更新了实体页面。
- 验证 Agent 创建或更新了将此资料源与现有知识连接起来的主题概览页面(如果 Wiki 已有内容)。
- 摄取关于同一主题但与第一个资料源中某些内容矛盾的第二个资料源。验证 Agent 在相关页面上标记了矛盾。
- 摄取后检查 Wiki 目录。它应该在 summaries、entities 和 topics 目录中包含新的或更新的文件。raw 目录应包含未修改的原始资料源。
- 摄取一个提及 Wiki 中尚不存在实体的资料源。验证 Agent 创建了存根页面或标记了空白。
5、构建索引和目录
在这一步中,你的目标是构建索引和日志——两个特殊文件,帮助 Agent(和你)随着 Wiki 的增长来导航它。
索引(index.md)是面向内容的。它是 Wiki 中每个页面的目录,按类别组织:实体、概念、资料源、概览。每个条目包含指向页面的链接、一行摘要,以及可选的元数据如创建日期和为其提供数据的资料源数量。当 Agent 需要回答查询时,它首先读取索引以找到候选页面,然后深入查看最相关的页面。这种方法在中等规模(数百个页面)下效果很好,避免了在浏览层面需要基于嵌入的 RAG 基础设施。
日志(log.md)是按时间顺序的。它是一个仅追加的记录,记录了发生的所有事情:摄取、查询、lint 检查、schema 更改。每个条目以一致的格式前缀开始:## [YYYY-MM-DD] type | Description。这使得日志可以用简单的命令行工具解析——grep "^## \\[" log.md | tail -5 给你最近五个条目。
关键的设计决策是 Agent 拥有这两个文件。每次摄取都应使用新页面和修订后的摘要更新索引。每个操作都应追加到日志。Agent 应在每次查询开始时读取索引以了解有哪些可用内容。Agent 应在每个会话开始时读取日志以了解最近做了什么。
将索引和日志维护构建到你 Step 2 的 LangGraph 工作流中。在 Agent 完成摄取的 Wiki 页面编写后,它应作为图中的最终节点更新索引并追加到日志。如果摄取中途失败,日志应记录失败。
测试:
- 摄取一个资料源并验证索引已更新,包含新摘要页面、实体页面和概念页面的条目。每个条目应有链接和一行描述。
- 摄取第二个资料源并验证索引反映了两个资料源,共享的实体页面显示更新的描述。
- 在几次摄取后检查日志。验证每个条目具有正确格式(
## [YYYY-MM-DD] ingest | Title)并按时间顺序出现。 - 运行
grep "^## \\[" log.md并验证你得到一个干净的时间序列列表。 - 手动删除一个 Wiki 页面。摄取新的资料源并验证索引处理了缺失的页面(移除过期条目而不是留下死链接)。
6、添加语义搜素
在这一步中,你的目标是使用向量嵌入和全文搜索为 Wiki 页面添加语义搜索,存储在 Oracle AI Database 中。
到目前为止,Agent 通过读取索引和跟随链接来导航 Wiki。这在中等规模下可以工作,但随着 Wiki 增长到数百个页面,你会需要语义搜索——按含义查找页面,而不仅仅是浏览目录。
对每个 Wiki 页面(排除索引和日志本身,以及排除原始资料源文件),使用嵌入模型生成向量嵌入,并将嵌入与页面的路径、标题、类型、标签和摘要一起存储在 Oracle Database 中。所有元数据字段都应被存储和索引,以便你可以按类型("仅实体页面")或按标签进行过滤。
在嵌入列上创建向量索引以进行快速的余弦相似度搜索。同时在页面内容(或至少在标题和摘要字段)上创建 Oracle Text 全文索引。向量搜索即使在不匹配单词的情况下也能找到语义相关的页面。全文搜索捕获精确的名称、技术术语和向量搜索可能排名较低的短语。两者结合为你提供强大的检索能力。
思考嵌入应该在什么时候生成。每次 Agent 在摄取过程中创建或更新页面时,新的或修订的页面需要重新嵌入并存储。未被摄取触及的页面应保留其现有嵌入。你需要跟踪哪些页面发生了变化,以便只重新嵌入那些页面。
还要思考你嵌入什么内容。你可以嵌入完整的页面文本,但长页面可能会稀释语义信号。你可以嵌入摘要或前 N 段。你可以分别嵌入标题和正文并合并分数。进行实验,找到适合你测试语料库的方法——不同的方法适用于不同类型的内容。
测试:
- 对你的测试 Wiki 运行完整管道:解析所有页面、生成嵌入、存储在 Oracle Database 中。验证存储的嵌入数量与 Wiki 页面数量匹配。
- 直接查询 Oracle Database 检查几个存储的条目。验证每个条目包含嵌入向量、页面路径、标题、类型和标签。
- 验证向量索引和全文索引都已创建。
- 更新一个页面(通过摄取修改现有实体页面的新资料源)。验证只有更改的页面被重新嵌入;未更改的页面保留其现有嵌入。
- 按页面讨论的概念(而非其精确标题)搜索页面。验证向量搜索即使词语不匹配也能返回它。
- 搜索精确的技术术语或人名。验证全文搜索以高置信度捕获它。
7、构建查询系统
在这一步中,你的目标是构建查询系统——通过搜索 Wiki 并合成响应来回答你问题的 Agent 工作流。
现在你有了一个可搜索的 Wiki,你需要让 Agent 利用它。当你提出问题时,Agent 应遵循一个多步过程:读取索引以识别候选页面、使用你在 Step 4 中构建的混合搜索搜索语义相关页面、完整读取最相关的页面,并合成带有引用特定页面和章节的答案。
将查询工作流建模为 LangGraph 图。节点可能包括:读取索引、混合搜索、读取候选页面和合成答案。如果 Agent 发现空白——问题涉及 Wiki 未能很好地覆盖的内容——它应诚实地说明,而不是猜测。
系统应根据问题支持不同的答案格式。两个概念之间的比较可能最适合表格形式。事件时间线可能最适合按时间顺序的列表。简单的解释可能最适合散文形式。给 LLM 选择格式的灵活性,并在你的提示中提供指导。
一个重要的能力:系统应提议将好的答案作为新页面保存到 Wiki 中。当你提出一个产生有用分析、比较或综合的问题时,Agent 应询问你是否要保存它。如果保存,它将答案写为新的 Wiki 页面,添加到索引,记录日志,并生成嵌入。这样你的探索就能像摄取的资料源一样在知识库中积累。
支持会话中的后续问题。如果你问"告诉我关于 X"然后"那 Y 呢?",Agent 应从上下文中理解 Y 与 X 所属的更广泛主题相关。LangGraph 的状态在查询之间传递对话上下文。
测试:
- 问一个关于你 Wiki 中覆盖良好的内容的问题。答案应准确,引用特定页面,且不捏造 Wiki 中没有的事实。
- 用不同的措辞问同一个问题。验证你得到类似的答案——语义搜索应按含义匹配,而非精确措辞。
- 问一个横跨两个或更多 Wiki 页面的问题。验证 Agent 读取多个页面并合成将它们连接起来的答案。
- 在不重述主题的情况下提出后续问题。验证 Agent 保持了上一次交流的上下文。
- 问一个关于你 Wiki 中未覆盖的内容的问题。验证 Agent 诚实地报告了空白,而不是编造内容。
- 要求 Agent 将答案保存为 Wiki 页面。验证页面已在磁盘上创建,添加到索引,记录日志,并在 Oracle Database 中生成了嵌入。
- 问一个比较问题("X 和 Y 有什么区别?")。验证答案使用了适当的格式(表格、并排对比等)。
8、添加Wiki项目管理
在这一步中,你的目标是添加 Wiki 项目管理,以便你可以为不同主题维护独立的知识库。
单个 Wiki 很有用,但你可能想为生活的不同领域建立独立的知识库——一个用于研究主题,一个用于读书笔记,一个用于健康和健身,一个用于职业学习。每个都应是隔离的,有自己的一组页面、自己的嵌入和自己的 Agent 记忆。
添加对命名 Wiki 项目的支持。在 Oracle AI Database 中存储项目元数据——名称、创建日期、上次摄取时间戳、页面数量、资料源数量。每个项目的嵌入和元数据应隔离,以便对一个 Wiki 的搜索永远不会返回另一个 Wiki 的页面。
用户应该能够创建新的 Wiki 项目、列出现有项目以及选择要使用的项目。当用户启动系统时,他们应该能够指定项目名称并立即从上次停止的地方继续。
所有 Wiki 数据应在运行之间持久化。Markdown 文件存储在其项目目录的磁盘上。嵌入和元数据存储在 Oracle AI Database 中。当用户回来并选择一个项目时,一切都应该完全保持原样——相同的页面、相同的索引、相同的日志、相同的搜索能力。
测试:
- 创建两个关于不同主题的 Wiki,每个都有少量资料源文档。对两者进行摄取。
- 查询一个 Wiki 并验证结果仅来自该 Wiki,而非另一个。
- 列出你的 Wiki 并验证两者都显示正确的名称和元数据(页面数量、上次摄取时间)。
- 直接在 Oracle Database 中查询项目元数据并验证它与系统报告的一致。
- 停止并重启你的系统。验证两个项目的所有 Wiki 数据完整且可查询。
- 向现有 Wiki 添加新资料源并重新运行摄取。验证只有新的或更改的页面被重新嵌入;未更改的页面保留其现有嵌入。
9、构建lint检查系统
在这一步中,你的目标是构建一个 lint 检查系统,对你的 Wiki 进行健康检查并帮助它在增长过程中保持一致性。
随着 Wiki 积累页面和资料源,不一致性会悄悄渗入。一个页面做出了一条被较新资料源反驳的声明,但旧页面从未更新。一个概念在五个页面中被讨论但从未有自己的专属页面。一个页面引用了另一个已被重命名或删除的页面。链接只是单向的。你只有一半故事的地方出现了空白。人类放弃 Wiki 是因为这种维护负担增长得比价值更快。Agent 可以处理它。
将 lint 操作构建为 LangGraph 工作流。Agent 应系统地遍历 Wiki 并检查:页面之间的矛盾(两个页面做出不兼容的声明)、过时的声明(一个页面断言了较新资料源已修订或反驳的内容)、孤立页面(没有其他 Wiki 页面链接到的页面)、缺失的页面(被引用但缺少自己页面的实体或概念)、损坏的交叉引用(指向不存在页面的链接),以及数据空白(Wiki 内容薄弱、可能受益于额外资料源的区域)。
Agent 应将其发现报告为优先列表:首先是关键问题(矛盾、过时声明),然后是警告(孤立页面、缺失页面),然后是建议(空白、可能需要查找的新资料源)。每个问题应包括涉及的具体页面和建议的操作。
Lint 检查还应建议要调查的新问题和要查找的新资料源——Wiki 中缺少什么能填补重要的空白?这使 lint 操作从错误发现练习变成了一个研究规划工具。
默认使 lint 操作为交互式。Agent 呈现其发现,你在进行任何更改之前接受、拒绝或修改每个建议。Agent 不应在没有确认的情况下修改页面,除非你明确运行自动修复模式。
测试:
- 创建一个故意的矛盾:编写两个对同一事物做出不兼容声明的实体页面。运行 lint 并验证 Agent 检测并报告了矛盾,引用了两个页面。
- 创建一个孤立页面:一个没有其他页面链接到它的页面。运行 lint 并验证它被标记。
- 引用一个不存在的页面(一个损坏的 wikilink)。运行 lint 并验证损坏的链接被报告。
- 让一个概念在多个页面中被讨论但没有自己的专属页面。运行 lint 并验证 Agent 建议创建一个。
- 接受一个 lint 建议并验证 Agent 正确应用了修复。
- 拒绝一个 lint 建议并验证没有进行任何更改。
10、构建用户界面
在这一步中,你的目标是为你的 LLM Wiki 构建一个用户界面,提供一个基于聊天的界面来查询和管理你的知识库。
你的 Wiki Agent 目前通过开发 CLI 工作。现在给它一个正式的界面。你可以选择:构建 Web 界面(类似于 Code Sherpa 挑战)、CLI 聊天界面,或两者兼有。无论哪种方式,核心需求都是相同的。
界面应提供一个用于查询 Wiki 的聊天面板。用户用自然语言输入问题,Agent 以引用特定 Wiki 页面的合成答案回复。响应应渲染 Markdown,以便表格、列表和格式化文本清晰显示。引用应是可点击的链接,打开引用的页面。
界面应支持你在 Step 5 中构建的完整查询工作流:语义搜索、多页面合成、带上下文的后续问题、不同的答案格式,以及将答案保存为新的 Wiki 页面。
提供一种浏览 Wiki 结构的方式:一个页面树或列表,显示类别(实体、概念、摘要、概览)和每个类别中的页面。这帮助用户一目了然地了解知识库的形态。
当 Agent 处理查询时,显示加载状态,让用户知道正在发生某些事情。LangGraph 工作流可能需要几秒钟,因为 Agent 读取索引、搜索页面、读取它们并合成答案。
构建一个单独的 CLI 工具用于资料摄取,不需要启动完整界面。用户应该能够运行类似 llm-wiki ingest my-research ./new-article.md 的命令,让 Agent 在后台处理资料源,更新 Wiki、索引、日志和嵌入。这使得添加资料源成为一个快速、低摩擦的操作。
测试:
- 启动界面并验证你可以访问它。
- 选择一个 Wiki 项目并问一个问题。验证响应带有 Markdown 渲染和 Wiki 页面引用。
- 点击引用链接。验证它打开了引用的页面。
- 提出后续问题并验证系统保持了上一次交流的上下文。
- 通过界面浏览 Wiki 结构。验证它准确反映了磁盘上的页面。
- 要求 Agent 将答案保存为 Wiki 页面。验证页面立即出现在 Wiki 结构中。
- 使用 CLI 摄取工具添加新的资料源。验证 Wiki 更新而无需启动完整界面。
- 提交查询并验证在 Agent 处理时出现加载指示器。
11、更进一步
你已经构建了一个可工作的个人知识库,其中 LLM Agent 摄取资料源、编写 Wiki 页面、回答问题并保持一切一致。以下是一些进一步发展的方向:
- 云数据库支持: 添加一个选项以连接到云托管的 Oracle AI Database 实例,而非本地 Docker 容器。从配置中读取连接字符串。
- 批量摄取: 添加一种批量模式,一次摄取多个资料源,减少监督。Agent 处理每个资料源,生成更新,并呈现更改摘要供你审阅,而不是逐一讨论每个资料源。
- Marp 幻灯片生成: 添加从 Wiki 内容生成幻灯片的能力(使用 Marp 格式)。这只需一个请求就能将一个主题的一组页面变成演示文稿。
- Obsidian 集成: 构建与 Obsidian 的更紧密集成。监视 Wiki 目录中通过 Obsidian 做出的更改并自动更新嵌入。使用你的 Agent 已经编写的 frontmatter 配合 Obsidian 的 Dataview 插件。
- 多格式资料源: 扩展资料摄取以处理 PDF、Web URL(带抓取)和音频转录。支持的格式越多,你能捕获的知识就越多。
原文链接: Coding Challenge #123 - Database Driven LLM Wiki
汇智网翻译整理,转载请标明出处