作者在开发内部知识助手 Guidely 时,总结了文档解析、Chunking、Embedding、向量检索到生成答案的完整流水线设计思路。
构建一个 AI 应用很容易描述:
上传文档 → 提问 → 获取答案。
真正去实现这个流程却是另一回事。
在开发 Guidely(一个内部知识助手)时,我想了解这两个端点之间究竟发生了什么。更重要的是,我希望设计一个系统,让每个部分都有清晰的职责,并且可以独立测试。
最终我构建了一个端到端的检索增强生成(RAG)系统,围绕一条简单的流水线:
Documents
↓
Parsing
↓
Chunking
↓
Embeddings
↓
Vector Store
↓
Semantic Search
↓
Response Generation
↓
Citations
↓
React Frontend
有趣的不仅是让 LLM 回答问题,而是设计一个让整个流水线可靠运行的架构。
Guidely 是一个内部知识助手,允许用户对一组组织文档提问。
它并非指望 AI 模型已经了解组织内部知识的一切,而是从组织的文档中检索相关信息,并用这些信息来构建答案。
例如,用户可能会问:
"什么是 TrustLayer?"
Guidely 会在组织的知识库中搜索,检索最相关的章节,并使用这些章节作为生成答案的上下文。
回答会连同支撑它的来源一起呈现。
这就是 RAG 的基本思想。
但我希望架构让这个流程变得显式,而不是把一切隐藏在一个大函数里。
第一个重大决策是将系统分离成不同的阶段。
┌─────────────────┐
│ Documents │
└────────┬────────┘
↓
┌─────────────────┐
│ Parser │
└────────┬────────┘
↓
┌─────────────────┐
│ Chunker │
└────────┬────────┘
↓
┌─────────────────┐
│ Embeddings │
└────────┬────────┘
↓
┌─────────────────┐
│ Vector Store │
└────────┬────────┘
↓
User Query
↓
┌─────────────────┐
│ Semantic Search │
└────────┬────────┘
↓
┌─────────────────┐
│ Response │
└────────┬────────┘
↓
┌─────────────────┐
│ React Frontend │
└─────────────────┘
每个组件回答不同的问题:
这种分离成为项目中最重要架构决策之一。
第一阶段是将文档引入系统。
Guidely 支持以下类型的文档:
上传 API 接收文档并将其存储到文档目录。
但上传文件并不等于使其可搜索。
文档需要经过摄入流水线:
Uploaded document
↓
Determine file type
↓
Parse document
↓
Extract text
↓
Chunk text
↓
Generate embeddings
↓
Store vectors + metadata
这种分离意味着上传层不需要理解 embeddings 或语义搜索。它的职责很简单:
把文档引入系统。
一旦文档存在,Guidely 需要提取其中的文本。
Parser 提供了一个通用接口:
parse_document(file_path)
在内部,可以根据文件类型选择合适的解析器:
.txt → text parser
.pdf → PDF parser
.docx → DOCX parser
这里重要的架构思想是:流水线的其余部分不需要关心文本来自哪里。
解析完成后,所有下游处理都只面对:
text: str
这保持了流水线的格式独立性。
一篇文档可能有成千上万字。
将整个文档送入检索系统并不理想。
相反,Guidely 将提取的文本拆分成更小的块。
分块策略使用 token 而不是简单地按字符数切分:
Document
────────────────────────────
Paragraph 1
Paragraph 2
Paragraph 3
Paragraph 4
Paragraph 5
...
Chunk 1
────────────────
Paragraph 1
Paragraph 2
Chunk 2
────────────────
Paragraph 2
Paragraph 3
Chunk 3
────────────────
Paragraph 3
Paragraph 4
重叠是有意的。
如果一个相关句子恰好位于块边界附近,重叠可以减少重要上下文被分离的几率。
因此分块函数有两个重要参数:
chunk_size = 800
overlap = 100
具体数值可以后续调整。
重要的设计决策是将分块作为一个独立的服务,而不是把逻辑嵌入文档摄入流程中。
这就是系统从传统文本处理进入语义搜索的地方。
每个块被转换成向量表示:
"TrustLayer is a decentralized protocol..."
↓
Embedding Model
↓
[0.021, -0.143, 0.782, ...]
用户提问时也会发生相同的过程:
"What is TrustLayer?"
↓
Embedding Model
↓
Query Vector
现在系统可以比较查询向量和文档向量。
这就是语义检索的基础。
我在这里遇到的一个挑战是模型选择。
我最初探索了托管的 Embedding API,但遇到了 API 限制。最终我转向了本地 Sentence Transformers 模型。
这个决策除了解决眼前问题外,还有架构上的好处:embedding 层变得与应用其余部分独立了。
如果以后要更换 embedding 模型,搜索 API 不需要改变。
一旦块有了 embeddings,这些向量需要存储在某个地方。
对于 Guidely,我使用了 FAISS。
基本关系如下:
Vector
│
├── FAISS index
│
└── Metadata
├── filename
├── chunk information
└── original text
向量索引处理相似性搜索。
元数据提供理解向量代表什么所需的信息。
这种分离很重要。
向量数据库会回答:哪些向量与这个查询最接近?
元数据回答:这些向量实际上代表什么?
当用户提交问题时,查询遵循一条更短的路径:
User question
↓
Create embedding
↓
FAISS similarity search
↓
Top K results
↓
Relevant document chunks
搜索服务大致如下:
query_embedding = create_embeddings([query])[0]
distances, indices = index.search(
query_vector,
top_k
)
返回的向量 ID 然后被映射回文档元数据。
这个架构有一个有用的特性:搜索服务不需要知道前端的任何信息。
它只返回结构化的结果:
{
"filename": "faq.txt",
"text": "TrustLayer is a decentralized protocol..."
}
这保持了后端边界的清晰。
我遇到的一个重要问题是处理不相关的问题。
向量数据库通常总会返回一些东西。
即使用户问了一个与知识库完全无关的问题,FAISS 仍然可以返回最近的向量。
这就造成了一个危险的情况:
Irrelevant question
↓
Similarity search
↓
Some vaguely similar chunks
↓
AI generates an answer
↓
Citation appears
因此系统即使在不应该回答时也可能看起来很自信。
这引出了一个重要的架构需求:
检索需要一个相关性边界。
不是盲目接受前 K 个结果,系统需要判断检索到的结果是否真的足够相关以支撑一个回答。
这也是需要仔细处理 citations 的地方。
citation 不应该仅仅因为文档被 FAISS 返回就出现。
它应该出现,是因为那篇文档确实为答案提供了相关的上下文。
检索之后,相关块成为响应层的上下文。概念上:
User Question
+
Retrieved Context
↓
Response Generator
↓
Answer + Sources
后端返回结构化的响应,而不是暴露内部实现细节:
{
"answer": "TrustLayer is a decentralized protocol on Solana...",
"citations": [
{
"source": "faq.txt",
"snippet": "It allows clients and talent to collaborate directly..."
}
]
}
这个区别很重要。后端可以包含 embedding 向量等内容,但用户不需要看到其中任何一个。响应层充当内部检索系统和人机应用之间的边界。
Citations 成为这个项目有趣的一部分。
最初,返回整个检索到的块效果很差。
一个 citation 可能包含整个文档章节,即使只有一个句子支撑了答案:
faq.txt
TRUSTLAYER FREQUENTLY ASKED QUESTIONS
...
TABLE OF CONTENTS
...
Q1...
Q2...
Q3...
这技术上是一个 citation,但对人类来说不太有用:
faq.txt
"It allows clients and talent to collaborate directly..."
citation 应该回答:
"这个信息从哪里来?"
而不是:
"这里是一大段文档。"
这引出了基于查询的 citation 策略——根据与用户问题相关的信息来选择片段。
前端有意与检索系统分离。
我用 React 构建了界面:
Guidely
Ask your organization's knowledge
┌──────────────────────────────────────────┐
│ Ask a question... → │
└──────────────────────────────────────────┘
Answer
TrustLayer is a decentralized protocol...
Sources
┌──────────────────────────────────────────┐
│ 📄 faq.txt │
│ It allows clients and talent... │
└──────────────────────────────────────────┘
前端不需要知道 embeddings 如何工作。
它不知道 FAISS 是什么。
它不需要理解分块。
它只是消费 API 的响应契约。
这种分离让系统更容易推理。
第二个主要前端界面是知识库管理页面。管理员界面允许上传和查看文档。架构如下:
Admin
↓
Upload document
↓
FastAPI
↓
Document storage
↓
Ingestion pipeline
↓
Embeddings
↓
FAISS
界面有意隐藏了实现细节。
管理员不需要知道:
"您的文档已被转换为 768 维向量并插入索引 42。"
只需要知道:
"您的文档已上传并可用。"
这个区别影响了很多 UI 决策。
FastAPI 成为前端和内部服务之间的边界。
应用围绕职责组织:
app/
├── routers/
│ ├── search.py
│ └── documents.py
│
├── services/
│ ├── parser.py
│ ├── chunker.py
│ ├── embeddings.py
│ ├── vector_store.py
│ └── response.py
│
└── main.py
这个结构不是为了创建尽可能多的文件。
而是为了让数据流变得可理解。
搜索请求可以追溯:
search router
↓
search service
↓
embedding service
↓
vector store
↓
response service
类似地,文档摄入有自己的路径。
这让调试变得相当简单。
最大的挑战不是写单个函数。
而是决定每个职责应该放在哪里。
例如,创建一个大的函数是可能的:
def ask_question(query):
# create embedding
# search FAISS
# retrieve documents
# generate response
# format citations
# return result
它可能会工作。
但它也会变得难以测试和修改。
相反,Guidely 分离了这些职责。
这让我能够独立更改分块策略或响应格式,而不必重写整个系统。
这就是架构目标。
把所有东西整合在一起后,最终架构如下:
DOCUMENT INGESTION
Document
↓
Parser
↓
Text
↓
Chunker
↓
Chunks
↓
Embedding Model
↓
Vectors
↓
FAISS + Metadata
│
│
│
▼
QUERY PIPELINE
User Question
↓
Embedding Model
↓
Query Vector
↓
FAISS Similarity Search
↓
Relevant Chunks
↓
Relevance Filtering
↓
Response Generation
↓
Answer + Query-Aware Citations
↓
React UI
这就是让 Guidely 感觉像一个完整系统而不是仅仅一个 AI 聊天机器人的原因。
当前架构可以工作,但随着项目发展,有几个方面我会改进:
更好的检索评估
仅靠相似度分数是不够的。
我想构建一个包含预期相关文档的proper评估数据集。这会让检索质量可衡量,而不是靠手动评估。
结构感知分块
不同文档有不同结构。
固定的基于 token 的分块大小不一定对以下内容最优:
未来的版本可以使用结构感知的分块。
更好的 citation 提取
Citation 片段可以根据查询和生成的答案更智能地选择。
持久的向量基础设施
FAISS 对这样的项目效果很好,但生产部署可以根据规模和运维需求受益于持久化的向量数据库。
认证和权限
当前的管理功能主要专注于文档管理。
生产级知识助手还需要认证、授权、文档所有权,以及可能的按用户或按团队的知识库。
这个项目最有价值的部分不是让 AI 模型回答问题。
而是学会把系统看作独立阶段的集合。
一个有用的心智模型是:
不要从以下问题开始:
"如何让 AI 回答问题?"
从以下问题开始:
"信息如何在系统中流动?"
一旦这个问题被回答,架构就变得清晰多了。
文档变成文本。文本变成块。块变成向量。向量变成可搜索的知识。搜索结果变成上下文。上下文变成答案。
而答案变成人类真正可以使用的东西。
这就是 Guidely 背后的架构:一个小型但完整的端到端 RAG 系统,不仅为了工作而设计,而是为了让每个阶段都可理解、可替换、可测试。
下一个挑战是衡量每个阶段的效果。