背景与问题
企业知识库问答这个场景,几乎是 AI 智能体最容易“看起来简单,做起来很脏”的方向之一。
很多团队一开始的想法都很直接:把内部文档丢进向量库,再接一个大模型,不就能问答了吗?我一开始也是这么想的。结果上线后常见问题很快出现:
- 答得像那么回事,但引用不到原文
- 跨部门术语太多,召回经常偏
- 同一个问题,不同时间回答不一致
- 命中旧文档,答案过时
- 权限隔离不严,存在数据越权风险
- 效果评估靠“体感”,不知道到底有没有变好
所以,企业知识库问答真正的难点,不是“能不能答”,而是:
- 能否稳定召回正确上下文
- 能否在正确上下文上生成可信答案
- 能否可观测、可评估、可持续迭代
- 能否满足企业环境下的安全、权限、成本与性能要求
这也是为什么在企业里,单纯的“LLM + 向量检索”往往不够,必须往RAG(Retrieval-Augmented Generation)+ Agent 编排的方向走。
本文我从架构落地的角度,带你完整走一遍:从问题定义、架构设计、代码实现,到评估方法、常见坑和最佳实践。
方案概览:为什么是 RAG + 智能体
先说结论:如果你的企业知识库问答具备以下特征,RAG + 智能体通常是更靠谱的路线:
- 文档来源多:Wiki、PDF、工单、FAQ、制度文档、产品手册
- 问题类型复杂:事实查询、流程问答、策略解释、故障定位
- 有权限边界:部门、岗位、租户、项目隔离
- 需要可追溯:答案要带引用、支持审计
- 需要扩展动作:搜索、数据库查询、调用内部 API、工单创建
其中:
- RAG 解决“让模型基于企业知识回答”
- 智能体(Agent) 解决“何时查知识、查什么、是否追问、是否调用工具”
可以把它理解成两个层次:
- 知识层:把企业文档变成可检索、可引用、可更新的知识底座
- 决策层:让智能体根据任务决定使用哪个知识源、是否重写问题、是否执行工具链
核心原理
1. 企业知识库问答的基本链路
一个成熟的企业级 RAG 系统,通常不是“一次检索 + 一次生成”,而是一条流水线:
- 文档采集与清洗
- 分块与元数据提取
- 向量化与索引
- 查询理解与重写
- 混合召回
- 重排(Rerank)
- 上下文压缩
- 答案生成
- 引用与置信度输出
- 评估与反馈闭环
下面这张图可以先建立整体认知。
flowchart TD
A[企业文档源<br/>Wiki/PDF/FAQ/工单] --> B[清洗与切分]
B --> C[Embedding 向量化]
B --> D[关键词索引 BM25]
C --> E[向量检索]
D --> F[关键词检索]
E --> G[召回合并]
F --> G
G --> H[Rerank 重排]
H --> I[上下文压缩]
I --> J[LLM 生成答案]
J --> K[答案+引用+置信度]
K --> L[评估与反馈闭环]
2. 为什么企业场景要做“混合召回”
只做向量检索,容易漏掉以下内容:
- 专有名词、版本号、错误码、接口名
- 很短但关键的制度条款
- 数字、时间、型号等结构化信息
只做关键词检索,又会漏掉:
- 同义表达
- 语义改写
- 自然语言的模糊提问
所以企业场景里更稳的做法通常是:
- BM25 / 关键词检索:兜住精确术语
- 向量检索:兜住语义相似
- Rerank:把最终候选重新排序,提高前几条的命中率
这是很多团队从“能用”迈向“稳定可用”的关键一步。
3. 智能体在这里到底做什么
很多人把智能体理解成“会自动调用很多工具的模型”,但在知识问答里,Agent 的价值更实际:
- 判断问题是不是知识库型问题
- 是否需要先澄清用户意图
- 是否重写问题再检索
- 是否跨多个知识源检索
- 是否切换到数据库/API 工具
- 是否根据召回质量拒答或降级输出
也就是说,Agent 更像一个问答流程编排器,而不是花哨的“全自动机器人”。
下面用时序图看一下。
sequenceDiagram
participant U as 用户
participant A as 智能体
participant R as 检索层
participant K as 知识库
participant L as 大模型
U->>A: 提问
A->>A: 意图识别/权限校验/问题重写
A->>R: 发起混合检索
R->>K: 查询文档片段
K-->>R: 返回候选片段
R-->>A: 返回重排结果
A->>L: 提供上下文+回答指令
L-->>A: 生成答案与引用
A-->>U: 输出答案/引用/不确定性提示
4. 一个实用的分层架构
我更推荐把系统拆成四层,而不是把所有逻辑塞进一个服务里。
接入层
- Chat UI
- 企业 IM(飞书、钉钉、Slack)
- API Gateway
智能体编排层
- 意图分类
- 问题重写
- 工具选择
- 多轮状态管理
- 拒答策略
知识服务层
- 文档解析
- Chunk 管理
- Embedding
- Hybrid Search
- Rerank
- 权限过滤
基础设施层
- 对象存储
- 向量数据库
- 搜索引擎
- 观测系统
- 审计日志
- 缓存
flowchart LR
subgraph Access[接入层]
A1[Web/IM/API]
end
subgraph Agent[智能体编排层]
B1[意图识别]
B2[问题重写]
B3[工具路由]
B4[多轮状态]
end
subgraph Knowledge[知识服务层]
C1[文档解析]
C2[Chunk/元数据]
C3[混合检索]
C4[Rerank]
C5[权限过滤]
end
subgraph Infra[基础设施层]
D1[对象存储]
D2[向量库]
D3[搜索引擎]
D4[缓存]
D5[监控与审计]
end
A1 --> B1
B1 --> B2
B2 --> B3
B3 --> C3
C1 --> C2
C2 --> D2
C2 --> D3
C3 --> C4
C4 --> C5
C5 --> B4
D4 --> B3
D5 --> B4
方案对比与取舍分析
方案一:纯向量检索 + LLM
优点
- 实现最快
- 原型验证成本低
缺点
- 对术语、编号、短文本不稳定
- 很难做权限精细控制
- 评估与调优空间有限
适用
- PoC
- 文档规模不大、术语不复杂的团队
方案二:混合检索 + Rerank + LLM
优点
- 企业问答命中率明显更稳
- 对复杂文档结构更友好
- 易做分层优化
缺点
- 链路更长,成本和延迟略高
- 需要维护更多组件
适用
- 大多数正式上线项目
方案三:RAG + Agent + 工具链
优点
- 能处理“问答 + 执行动作”的复合场景
- 多轮对话与工具协同能力更强
- 更接近企业生产环境需求
缺点
- 编排复杂度高
- 调试成本明显上升
- 如果边界没收好,容易“过度智能化”
适用
- 复杂企业内部助手
- 跨知识库、跨系统、跨流程场景
我的建议
如果你现在正准备落地,不要一步冲到“全能智能体”。
更稳的路线通常是:
- 先做高质量 RAG
- 把评估体系补齐
- 再在局部引入 Agent 决策
- 最后扩展工具调用
很多项目失败,不是因为模型不够强,而是因为一开始就把系统复杂度拉太高。
容量估算与架构边界
企业知识库系统上线前,最好先粗估三个量:
1. 文档规模
例如:
- 10 万篇文档
- 平均每篇切成 20 个 chunk
- 总 chunk 数约 200 万
如果每个向量 1536 维,按 float32 粗估:
- 单向量约
1536 * 4 = 6144 bytes ≈ 6 KB - 200 万 chunk 仅向量数据约
12 GB - 再加索引、元数据、冗余,实际要更高
这意味着:
- 小团队可以先用托管向量库
- 中大型团队要认真评估索引类型、冷热分层、分库分表
2. 查询吞吐
比如工作日高峰:
- QPS 20
- 每次查询 TopK=20
- 含重排与 LLM 生成
瓶颈通常出现在:
- Rerank 模型
- 大模型响应
- 权限过滤
- 热门问题重复请求
所以缓存非常重要,尤其是:
- Query Rewrite 缓存
- 检索结果缓存
- 热门问答缓存
3. 更新频率
如果知识更新很快,比如制度、价格、产品策略每天变:
- 不能只靠离线全量构建
- 要支持增量同步
- 要有文档版本控制
- 要有失效与回滚机制
实战代码(可运行)
下面给一个可运行的 Python 示例,演示一个最小化的企业知识库 RAG 流程:
- 文档切分
- TF-IDF 向量化
- 检索 TopK
- 简单 Agent 路由
- 生成带引用的答案
这个例子不依赖外部大模型,方便本地验证核心链路。生产环境你可以把检索器替换成 Elasticsearch + 向量库,把回答器替换成企业可用的 LLM。
运行环境
pip install scikit-learn numpy
示例代码
from dataclasses import dataclass
from typing import List, Dict, Tuple
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import re
@dataclass
class Chunk:
doc_id: str
title: str
text: str
source: str
department: str
class SimpleRAG:
def __init__(self, chunks: List[Chunk]):
self.chunks = chunks
self.vectorizer = TfidfVectorizer()
self.texts = [f"{c.title}\n{c.text}" for c in chunks]
self.matrix = self.vectorizer.fit_transform(self.texts)
def search(self, query: str, top_k: int = 3, department: str = None) -> List[Tuple[Chunk, float]]:
candidate_chunks = self.chunks
candidate_indices = list(range(len(self.chunks)))
if department:
filtered = [(i, c) for i, c in enumerate(self.chunks) if c.department == department]
if not filtered:
return []
candidate_indices = [i for i, _ in filtered]
candidate_chunks = [c for _, c in filtered]
qv = self.vectorizer.transform([query])
sims = cosine_similarity(qv, self.matrix[candidate_indices]).flatten()
ranked = sorted(zip(candidate_chunks, sims), key=lambda x: x[1], reverse=True)
return ranked[:top_k]
class SimpleAgent:
def __init__(self, rag: SimpleRAG):
self.rag = rag
def rewrite_query(self, query: str) -> str:
# 简单规则模拟 query rewrite
query = query.strip()
query = query.replace("报销怎么弄", "差旅报销流程是什么")
query = query.replace("VPN 连不上", "VPN 连接失败如何处理")
return query
def classify_intent(self, query: str) -> str:
if any(k in query for k in ["流程", "制度", "规范", "怎么", "如何", "报销", "请假", "VPN"]):
return "knowledge_qa"
return "fallback"
def answer(self, query: str, department: str = None) -> Dict:
intent = self.classify_intent(query)
if intent != "knowledge_qa":
return {
"intent": intent,
"answer": "这个问题更像开放式对话,建议切换到通用模型处理。",
"citations": []
}
rewritten = self.rewrite_query(query)
results = self.rag.search(rewritten, top_k=3, department=department)
if not results or results[0][1] < 0.05:
return {
"intent": intent,
"answer": "我没有在当前权限范围内找到足够可靠的知识依据,建议补充关键词或确认所属部门。",
"citations": []
}
top_texts = []
citations = []
for chunk, score in results:
top_texts.append(f"[{chunk.title}] {chunk.text}")
citations.append({
"doc_id": chunk.doc_id,
"title": chunk.title,
"source": chunk.source,
"score": round(float(score), 4)
})
answer = self.synthesize_answer(query, top_texts, citations)
return {
"intent": intent,
"rewritten_query": rewritten,
"answer": answer,
"citations": citations
}
def synthesize_answer(self, query: str, contexts: List[str], citations: List[Dict]) -> str:
# 这里用规则模拟“基于上下文生成”
merged = "\n".join(contexts)
if "报销" in query:
return (
"根据知识库内容,差旅报销通常分为 4 步:"
"1)在出差结束后提交报销单;"
"2)上传发票、行程单等凭证;"
"3)由直属主管审批;"
"4)财务复核后打款。"
"如金额超出标准,需要补充说明。"
f" 参考文档:{', '.join([c['title'] for c in citations])}"
)
if "VPN" in query:
return (
"根据当前检索到的文档,VPN 连接失败可先按以下顺序排查:"
"1)确认账号未过期;"
"2)检查二次认证;"
"3)尝试切换网络;"
"4)重装客户端;"
"5)仍失败则联系 IT 服务台。"
f" 参考文档:{', '.join([c['title'] for c in citations])}"
)
preview = re.sub(r"\s+", " ", merged)[:180]
return f"我基于检索到的知识给出答案,核心依据如下:{preview}"
def build_demo_agent() -> SimpleAgent:
chunks = [
Chunk(
doc_id="hr-001",
title="差旅报销制度",
text="员工出差结束后 7 个工作日内提交报销申请,上传发票、行程单等凭证,由直属主管审批,财务复核后付款。超标费用需说明原因。",
source="hr_wiki",
department="HR"
),
Chunk(
doc_id="it-001",
title="VPN 连接失败处理手册",
text="VPN 无法连接时,先确认账号状态与 MFA 二次认证是否正常,再检查本地网络与客户端版本,必要时联系 IT 服务台。",
source="it_wiki",
department="IT"
),
Chunk(
doc_id="fin-001",
title="发票规范要求",
text="报销凭证需包含合法发票,电子发票需清晰可验真,缺失抬头或税号时财务有权退回。",
source="finance_wiki",
department="HR"
),
]
rag = SimpleRAG(chunks)
return SimpleAgent(rag)
if __name__ == "__main__":
agent = build_demo_agent()
queries = [
("报销怎么弄", "HR"),
("VPN 连不上怎么办", "IT"),
("帮我写一首诗", "HR"),
("报销怎么弄", "IT"),
]
for q, dept in queries:
result = agent.answer(q, department=dept)
print("=" * 80)
print("问题:", q)
print("部门:", dept)
print("结果:", result)
运行后你会看到什么
这个小例子展示了几个企业里很关键的能力:
- 问题重写:把口语化提问变成更适合检索的表达
- 权限过滤:按部门过滤知识范围
- 拒答策略:检索不到时不乱答
- 引用输出:答案带上来源文档
虽然它很简化,但系统的骨架已经在了。真实项目里,你可以逐步替换:
TfidfVectorizer→ 向量模型 + 混合检索- 规则路由 → LLM Router / 分类模型
- 规则回答器 → 企业大模型
- 内存列表 → 向量库 + 搜索引擎 + 元数据库
效果评估:别再只靠“感觉还行”
RAG 项目最容易被忽略的部分,就是评估。
很多团队上线后只收两类反馈:
- 用户觉得“还可以”
- 老板觉得“有时不太准”
这远远不够。企业系统必须把评估拆开来看。
1. 评估分层
检索层指标
关注“有没有找到对的内容”:
- Recall@K
- MRR
- NDCG
- 权限过滤后命中率
- Chunk 覆盖率
生成层指标
关注“有没有基于检索内容正确回答”:
- 引用一致性
- 忠实性(Faithfulness)
- 答案完整性
- 拒答准确率
- 幻觉率
业务层指标
关注“业务上有没有价值”:
- 首问解决率
- 人工转接率
- 平均处理时长
- 用户满意度
- 热门问题覆盖率
2. 建议的离线评估集构建方式
实际操作上,我建议至少准备三类样本:
-
标准 FAQ 类
- 问题明确
- 答案唯一
- 用来做基准线
-
模糊表达类
- 同义词、口语化、缩写
- 检验 Query Rewrite 和召回稳不稳
-
高风险类
- 权限敏感
- 时效敏感
- 容易答错造成业务影响
评估集最好包含:
- 问题
- 标准答案
- 期望引用文档
- 权限上下文
- 是否允许拒答
3. 一个简化版评估流程
stateDiagram-v2
[*] --> 构建评测集
构建评测集 --> 执行批量问答
执行批量问答 --> 检索评估
执行批量问答 --> 生成评估
检索评估 --> 失败样本归因
生成评估 --> 失败样本归因
失败样本归因 --> 调整切分策略
失败样本归因 --> 调整召回参数
失败样本归因 --> 调整提示词
调整切分策略 --> 回归测试
调整召回参数 --> 回归测试
调整提示词 --> 回归测试
回归测试 --> [*]
4. 我比较推荐的归因顺序
一条回答错了,不要第一时间怪模型。
先按这个顺序查:
- 没召回到对的 chunk
- 召回到了,但排序太靠后
- 上下文太长,关键信息被淹没
- 提示词没要求严格基于引用回答
- 模型本身不够稳
- 答案后处理逻辑有问题
这个顺序能省掉很多无效调参。
常见坑与排查
下面这些坑,我基本都见过,有些还亲手踩过。
1. Chunk 切太大,召回看似准,生成反而不准
现象
- 检索结果标题对了
- 但答案经常抓不到关键细节
原因
chunk 太大,关键信息被无关上下文稀释。
处理建议
- 一般先从 300~800 中文字试起
- 保留 10%~20% overlap
- 按标题、段落、列表、表格边界切分
2. 文档清洗不彻底,脏数据比模型问题更致命
现象
- 回答里出现乱码、页眉页脚、重复段落
- PDF 表格内容断裂
排查
- 随机抽样 50 个 chunk
- 人工检查是否含:
- 页码
- 水印
- 重复标题
- 错位文本
- OCR 错字
处理建议
- 文档解析单独做服务
- 保留原文与清洗结果双版本
- 建立“坏文档隔离队列”
3. 只做向量检索,错误码和制度编号总是找不到
现象
- 用户问“E2035 是什么错误”
- 系统答非所问
原因
纯语义检索对这类精确匹配不稳定。
处理建议
- 增加 BM25
- 对编号、错误码、产品名做实体抽取
- 实体命中时提升关键词召回权重
4. 权限过滤做在生成后,已经晚了
现象
- 模型输出了本不该看的信息
- 后处理再删字段,但敏感内容已经进过上下文
正确做法
权限控制必须前置到:
- 检索前过滤
- 或至少召回后、进入模型前过滤
核心原则
不要把模型当作权限边界。
5. 多轮对话越聊越偏
现象
第一轮还正常,第二轮开始答歪。
原因
- 历史对话全量拼接,噪声越来越多
- 没做对话状态摘要
- 上一轮错误假设被持续继承
处理建议
- 历史消息做摘要,不要无限累积
- 每轮都重新做检索,不要过度依赖旧上下文
- 对关键实体做槽位管理
安全/性能最佳实践
企业知识问答一旦上线,安全和性能不是“加分项”,而是准入门槛。
安全最佳实践
1. 基于文档级与 chunk 级双重权限控制
最少做到:
- 文档所属部门
- 用户角色
- 项目/租户隔离
- 文档密级
- 有效期控制
不要只在文档级做权限,必要时要支持 chunk 级权限标签。
2. 敏感信息脱敏与最小暴露
在进入模型前,对以下内容做脱敏或拦截:
- 手机号
- 身份证号
- 银行账号
- 客户隐私数据
- 商业敏感字段
实践上可以分两层:
- 索引前脱敏
- 生成前审查
3. 审计日志必须完整
建议记录:
- 用户 ID
- 会话 ID
- 原始问题
- 改写问题
- 检索结果 ID
- 最终引用
- 模型版本
- 响应时间
- 是否触发拒答
- 是否命中敏感规则
出了问题,这些日志就是你的“事故现场录像”。
性能最佳实践
4. 热点问题缓存
企业内部很多问题高度重复,比如:
- 假期制度
- VPN 问题
- 报销流程
- 权限开通
适合缓存的对象包括:
- 改写后的 query
- 检索结果 TopK
- 最终答案(带版本号和权限标签)
5. 异步索引与增量更新
不要每次文档更新都全量重建。
建议采用:
- 文档变更事件驱动
- 增量切分
- 增量 embedding
- 索引版本管理
- 回滚能力
6. 上下文压缩优先于盲目加大上下文窗口
很多人看到大模型支持超长上下文,就想“一把梭”全塞进去。实际生产里这通常不是好主意:
- 成本高
- 延迟高
- 噪声大
- 幻觉反而更严重
更靠谱的做法是:
- 检索 TopK 控制在合理范围
- 做 Rerank
- 提取关键句
- 保留结构化引用
7. 降级策略要提前准备
至少准备三层降级:
- Rerank 失败 → 直接使用召回结果
- LLM 超时 → 返回检索摘要
- 知识不足 → 明确拒答并引导人工支持
这类设计平时不起眼,真到高峰流量或模型波动时非常有用。
一个可落地的实施路径
如果你准备在企业里真正推进,我建议按下面四个阶段走。
第一阶段:验证可行性
目标是证明“能答对高频问题”。
- 选 1~2 个知识域
- 构建 100~300 条评测集
- 做基础混合检索
- 输出引用与拒答
验收标准
- 高频 FAQ 首问解决率达到可接受水平
- 幻觉率可控
- 用户愿意继续用
第二阶段:提升稳定性
目标是从“能用”变成“可靠”。
- 引入 Rerank
- 做 Query Rewrite
- 补权限过滤
- 建立观测与日志
- 做离线回归评测
验收标准
- 线上错误有归因路径
- 版本迭代能量化变好或变差
第三阶段:引入智能体编排
目标是覆盖更复杂问题。
- 加意图分类
- 加澄清问答
- 接入内部 API / DB 查询
- 做多轮状态管理
验收标准
- 多步问题成功率提升
- 工具调用边界明确
- 错误不会无限放大
第四阶段:规模化运营
目标是进入长期治理。
- 增量索引
- 多知识域治理
- 知识新鲜度监控
- 成本优化
- A/B 测试
验收标准
- 可持续迭代
- 成本可控
- 安全合规过审
总结
企业知识库问答不是一个“接上大模型就行”的项目,它更像一个知识工程 + 检索工程 + 编排工程 + 评估工程的组合体。
如果只记住几条,我建议你抓住下面这些:
- 先别急着做全能 Agent,先把 RAG 打稳
- 混合检索 + Rerank,通常比纯向量检索更适合企业场景
- 权限控制必须前置,模型不能充当安全边界
- 评估要拆成检索层、生成层、业务层
- 错答先查召回,再查提示词,最后才怪模型
- 上线前一定准备缓存、降级、审计日志和增量更新机制
最后给一个很实际的边界判断:
- 如果你的场景是高频 FAQ + 文档查询,先做高质量 RAG 就够了。
- 如果你的场景已经发展到问答 + 查系统 + 执行动作,再引入 Agent 会更划算。
- 如果你连评测集和权限模型都还没准备好,那先别急着扩功能,先把基础设施补齐。
真正能在企业里长期跑起来的,不一定是最炫的方案,而是最可控、最能持续优化的方案。