跳到主要内容

数据截至 (上游 commit dc85934f318c)

06 — 出口面

本章讲什么: 前五章讲的是索引本身。这一章讲它怎么被用起来——四种调用面,以及纯向量检索之外的三条旁路。


1. CLI

1.1 子命令一览

全部注册在 LeannCLI.create_parser(cli.py:251),add_subparserscli.py:290

命令干什么注册行
build建 / 增量更新索引cli.py:293
search语义搜索cli.py:511
ask基于索引问答cli.py:644
react多轮推理检索cli.py:732
watch常驻监听文件变更cli.py:456
rebuild强制重建cli.py:498
migrate迁移 passage ID 方案cli.py:478
warmup预热某索引的嵌入服务cli.py:589
daemon start/stop/status管理常驻嵌入服务cli.py:613-643
list / remove列出 / 删除索引cli.py:885 / :893
serve起 HTTP 服务cli.py:904
index-browser/email/imessage/wechat/chatgpt/claude/calendar各数据源一键建索引cli.py:842-884

1.2 索引存哪

CLI 把索引放在 <项目>/.leann/indexes/<索引名>/,里面固定叫 documents.leann.*(cli.py:242-249 的路径拼装,以及 _existing_index_artifacts 里写死的文件名 cli.py:83-88)。

leann list 会同时看两种布局:CLI 格式(.leann/indexes/)和 App 格式(散落的 *.leann.meta.json),后者靠深度受限的目录扫描发现(registry.py:25cli.py:1002)。

1.3 文档加载

load_documents(cli.py:1555)基于 LlamaIndex 的 SimpleDirectoryReader,并做了几件额外的事:

  • 跳过 git submodule 目录(cli.py:1574-1577,判定在 _is_git_submodule,:955)。
  • 尊重 .gitignore(_build_gitignore_parser,cli.py:920;_should_exclude_file,:943)。
  • PDF 走 pymupdf,失败再退 pdfplumber(extract_pdf_text_with_pymupdf,cli.py:177;..._pdfplumber,:199)。
  • suppress_cpp_output 屏蔽 C++ 后端刷屏,除非加了 -v(cli.py:114,开关在 :274-286)。

2. MCP 服务

2.1 形态

MCP(Model Context Protocol) 是让编码 agent 调用外部工具的协议。LEANN 的实现是一个 stdio 上的 JSON-RPC 循环,零依赖:逐行读 stdin、处理、把 JSON 打到 stdout(mcp.py:396-411)。入口是 leann_mcp 命令(packages/leann-core/pyproject.toml[project.scripts])。

2.2 四个工具

工具干什么处理函数
leann_search语义搜索,返回带文件路径和分数的代码块mcp.py:154
leann_list列出所有索引mcp.py:201
leann_build建 / 增量更新索引mcp.py:208
leann_status看某索引的后端 / 模型 / 块数 / 体积mcp.py:257

工具描述写得很"给 agent 看"——leann_search 的描述里直接给了四个查询例子,并提示"改代码前先用它找相关代码"(mcp.py:56-63)。

2.3 实现上的三个务实决定

① 不 import core,而是 shell 出去。 每个工具都 subprocess.run([sys.executable, "-m", "leann", ...])(mcp.py:11-34)。用 -m 而不是 leann 命令,是因为 Windows 上被 mcp-proxy 之类拉起时 PATH 里常常没有它(mcp.py:12-17 的 docstring)。

② 显式指定 UTF-8 解码。 text=True 会用系统 locale,中文 Windows 上是 GBK,而 CLI 输出带 emoji 和中文——会直接崩掉读取线程(mcp.py:26-31)。

③ 未知方法要回 -32601,通知则不回。 注释里给了具体案例:Google Antigravity CLI 会在 initialize 之前先探一个 server/discover,服务端不回就永远卡在 "initializing..."(mcp.py:370-381)。

2.4 搜索结果的格式化

--json --show-metadata --non-interactive 调 CLI,解析后拼成带 ### Result N — 文件路径 (score: x) 标题的 markdown 代码块(mcp.py:163-198)。JSON 解析失败就原样返回 stdout 兜底(mcp.py:181-184)。


3. ReAct agent

3.1 它是什么

ReAct = Reasoning + Acting:让 LLM 交替输出"想法"和"动作",执行动作拿到观察结果,再进入下一轮,直到给出最终答案。

ReActAgent(react_agent.py:26)支持三个工具:

工具干什么
leann_search("query")搜本地知识库
web_search("query")搜公网(需 Serper API key)
visit_page("url")抓完整网页(经 Jina Reader)

3.2 提示词按可用工具动态变形

没配 Serper key 时,提示词里根本不提 web 工具,只留 leann_search 并明说"网络搜索不可用"(react_agent.py:92-103)。这比"列出工具但调用时报错"干净得多。

3.3 解析靠正则,且容错

re.search(r'(web_search|leann_search|visit_page|search)\(["\']([^"\']+)["\']\)', action_part)

这是真实源码(react_agent.py:160-163)。裸的 search(...) 被归一成 leann_search(:166-167);连 Action: 这行都没有时,还会退化去全文找 search("...")(:169-172)。

3.4 跑满轮次也要给答案

达到 max_iterations(默认 5)仍没有最终答案时,把所有轮次的观察拼成一个大 prompt,让 LLM 直接总结(react_agent.py:286-297)。


4. 三条检索旁路

除了纯向量检索,LeannSearcher.search 还开了三个口子。

4.1 BM25 与混合检索

BM25 是经典的关键词打分算法(词频 × 逆文档频率,带长度归一)。LEANN 的实现挂在 SQLite 的 FTS5 全文索引上:

CREATE VIRTUAL TABLE bm25_passages USING fts5(
id UNINDEXED, text, tokenize='unicode61 remove_diacritics 2'
)

这是真实的建表语句(api.py:319-323)。查询时直接用 SQLite 的 bm25() 函数,并取负让它变成"越大越好",和向量侧对齐(api.py:361-366)。

选它的理由写在 docstring 里:词表和倒排表留在磁盘上,搜索时内存有界——替代了原先"全语料进内存"的实现(api.py:644-650)。

融合方式是最朴素的线性加权:

vector_weight行为
1.0(默认)纯向量
0.0纯 BM25(连向量都不算,api.py:1323-1330)
中间值w * 向量分 + (1-w) * BM25 分,降序取前 k(api.py:1408-1431)

注意 index 的边界清单第 ⑦ 条提到的符号问题:这个融合假定两边都是"越大越好"。

回退:如果建索引时没生成 FTS5 库,搜索时会现场从 passages 建一个(_init_bm25,api.py:1516-1568);目录不可写就报错并提示用 prebuild_bm25=True 重建(:1560-1565)。

还有一个废弃参数别踩:search(gemma=...)vector_weight 的历史别名("gamma" 的拼写错误),传了会 DeprecationWarning(api.py:1286-1295)。

4.2 元数据过滤

这是后过滤,不是预过滤——先做完向量检索拿到 top-k,再在结果上按条件筛(api.py:1473-1478)。所以过滤条件很严时,返回条数可能远少于 top_k

引擎是 MetadataFilterEngine(metadata_filter.py:20),支持的算子:

类别算子
比较== != < <= > >=
集合in not_in
字符串contains starts_with ends_with
布尔is_true is_false

格式是 {"字段名": {"算子": 值}},例如 {"chapter": {"<=": 5}, "tags": {"in": ["fiction"]}}(api.py:1270-1276 的文档)。

4.3 grep

use_grep=True 时完全绕过向量,直接 subprocess.run(["grep", "-i", "-n", query, jsonl_file]),按"出现次数"当分数排序(api.py:1593-1638)。适合找精确的错误信息、函数名。

两个限制:返回码 1(无匹配)返回空列表、其他非 0 抛错(api.py:1603-1606);而且文件名是写死的 documents.leann.passages.jsonl(api.py:1583-1586)。


5. 问答与 LLM provider

LeannChat.ask(api.py:1689)做的事很简单:搜 → 把命中原文用 \n\n 拼成 context → 套一个固定模板 → 交给 LLM(api.py:1734-1740)。它还会把每条命中的相关度 / ID / 前 60 字 / 来源打成一张对齐的日志表(api.py:1742-1752)。

支持的 LLM 后端由 get_llm(chat.py:1262)按配置分发:

provider
OllamaChat(chat.py:472)本地 Ollama
HFChat(:558)本地 HuggingFace
OpenAIChat(:789)OpenAI 及兼容服务
AnthropicChat(:874)Anthropic
GeminiChat(:735)Google Gemini
MiniMaxChat(:949) / NovitaChat(:1017) / AtlasCloudChat(:1087)其他云服务
SimulatedChat(:1146)测试用假实现

各家的 base_url 和 API key 解析统一收在 settings.py(resolve_openai_base_url 等,settings.py:45 起)。

一个复用细节: LeannChat 可以接收外部传入的 searcher;这种情况下它的 cleanup() 不会去停嵌入服务,以便多个 chat 共享同一个 searcher(api.py:1681-1686:1775-1778)。


6. 数据连接器(apps/)

apps/ 下是一批开箱示例,每个把某个数据源变成 LEANN 索引:

文件数据源
apps/document_rag.pyPDF / TXT / MD
apps/code_rag.py代码库(AST 分块)
apps/email_rag.pyApple Mail
apps/browser_rag.pyChrome 历史
apps/wechat_rag.py / apps/imessage_rag.py微信 / iMessage
apps/chatgpt_rag.py / apps/claude_rag.py / apps/gemini_rag.py / apps/qwen_rag.py各家 AI 对话存档
apps/slack_rag.py / apps/twitter_rag.py经 MCP 拉的实时数据
apps/colqwen_rag.py / apps/image_rag.py / apps/multimodal/多模态 / 视觉 PDF

它们共享 apps/base_rag_example.py 的骨架。CLI 里的 index-* 子命令是其中几个的封装(cli.py:842-884)。


7. 本章代码地图

主题文件符号
CLI 参数定义packages/leann-core/src/leann/cli.py:251LeannCLI.create_parser
文档加载packages/leann-core/src/leann/cli.py:1555load_documents
gitignore 处理packages/leann-core/src/leann/cli.py:920_build_gitignore_parser
索引列举packages/leann-core/src/leann/cli.py:1002list_indexes
MCP 工具定义packages/leann-core/src/leann/mcp.py:53TOOLS
MCP 请求分派packages/leann-core/src/leann/mcp.py:325handle_request
MCP CLI 调用packages/leann-core/src/leann/mcp.py:21_run_leann
ReAct 主循环packages/leann-core/src/leann/react_agent.py:182ReActAgent.run
ReAct 提示词packages/leann-core/src/leann/react_agent.py:69_create_react_prompt
动作解析packages/leann-core/src/leann/react_agent.py:129_parse_llm_response
网络搜索packages/leann-core/src/leann/web_search.pyWebSearcher
BM25 索引packages/leann-core/src/leann/api.py:310Fts5BM25Index
BM25 懒初始化packages/leann-core/src/leann/api.py:1516_init_bm25
grep 检索packages/leann-core/src/leann/api.py:1593_grep_search
元数据过滤packages/leann-core/src/leann/metadata_filter.py:20MetadataFilterEngine
问答packages/leann-core/src/leann/api.py:1689LeannChat.ask
LLM 分发packages/leann-core/src/leann/chat.py:1262get_llm
provider 配置解析packages/leann-core/src/leann/settings.py:27resolve_ollama_host