我给公司搭了个AI知识库管家:4个Agent协作回答专业问题(架构+踩坑)
我给公司搭了个AI知识库管家:4个Agent协作回答专业问题(架构+踩坑)
2026年,企业知识管理从"文档搜索"进化到"智能问答"。
但现实很骨感:员工问"我们公司的报销流程是什么",AI回答"请问您想了解哪方面的报销流程"。等于没说。
问题在哪?
单Agent直接RAG,检索不准、答案不专业、无法验证事实。
这篇文章基于真实项目经验(某制造业企业知识库项目,2025-2026年实施),讲清楚如何用4个Agent搭建一个生产级企业知识问答系统。
完整代码,直接能跑。
一、为什么单Agent做不好企业知识问答?
1.1 单Agent RAG的三大痛点
你用LangChain调一个RAG流程:
用户提问 → Embedding → 向量检索Top5 → 丢给LLM生成答案
痛点1:检索不准
用户问:“上海办事处的联系方式?”
向量检索返回:“上海办事处2023年年会活动总结.pdf”
完全不相关。
痛点2:答案不专业
用户问:“这个零件的加工公差是多少?”
LLM回答:“根据一般机械加工经验,公差通常在±0.05mm左右。”
错误。这个零件的公差是±0.01mm,在工艺文档里明确写了。
痛点3:无法验证事实
LLM会编造看起来合理的答案。比如把A产品的参数安到B产品上,你根本看不出来。
1.2 Multi-Agent的解决方案
把"回答问题"拆给4个Agent:
[Router Agent] → 理解问题类型,路由到对应知识库
↓
[Retriever Agent] → 多策略检索(向量+关键词+SQL)
↓
[Reader Agent] → 精读检索结果,提取答案线索
↓
[FactChecker Agent] → 验证答案事实性(对照原文)
↓
[输出最终答案+引用来源]
关键优势:
- 精准路由:技术类问题给技术Agent,人事类问题给人事Agent
- 多策略检索:不只靠向量,还靠关键词+SQL查询结构化数据
- 事实验证:FactChecker会标记"不确定"的答案(避免编造)
二、系统架构设计
2.1 整体架构
┌──────────────────────────────────────────┐
│ 企业知识问答Multi-Agent系统架构 │
├──────────────────────────────────────────┤
│ │
│ [用户输入] "CNC加工中心的操作规范?" │
│ ↓ │
│ [Router Agent] 识别问题类型 → 技术类 │
│ ↓ │
│ [Retriever Agent] │
│ ├→ 向量检索(Qdrant,技术文档库) │
│ ├→ 关键词检索(Elasticsearch) │
│ └→ SQL查询(工艺参数表) │
│ ↓ │
│ [Reader Agent] 精读Top10结果 │
│ → 提取答案 + 标注来源 │
│ ↓ │
│ [FactChecker Agent] 验证事实性 │
│ → 逐条对照原文 │
│ → 标记:confirmed / uncertain / false │
│ ↓ │
│ [输出答案 + 引用来源 + 置信度] │
│ │
│ 技术栈:LangGraph + Qwen3 + Qdrant + ES │
└──────────────────────────────────────────┘
2.2 状态定义
# state.py - Multi-Agent系统状态定义
from typing import TypedDict, List, Optional, Dict, Any
from enum import Enum
class QuestionType(Enum):
TECHNICAL = "technical" # 技术类(工艺参数、操作规范)
HR = "hr" # 人事类(报销、考勤、招聘)
PRODUCT = "product" # 产品类(规格、价格、交付周期)
POLICY = "policy" # 制度类(合规、安全、质量)
GENERAL = "general" # 通用类(闲聊、公司简介)
class ConfidenceLevel(Enum):
CONFIRMED = "confirmed" # 已确认(原文明确支持)
UNCERTAIN = "uncertain" # 不确定(原文间接支持)
INSUFFICIENT = "insufficient" # 信息不足(原文没有相关内容)
CONTRADICTED = "contradicted" # 矛盾(原文内容与答案冲突)
class KnowledgeState(TypedDict):
"""Multi-Agent系统状态"""
# 输入
user_question: str # 用户问题
# 路由阶段
question_type: Optional[QuestionType] # 问题类型
target_collections: Optional[List[str]] # 目标知识库集合
# 检索阶段
vector_results: Optional[List[Dict]] # 向量检索结果
keyword_results: Optional[List[Dict]] # 关键词检索结果
sql_results: Optional[List[Dict]] # SQL查询结果
merged_results: Optional[List[Dict]] # 合并后的检索结果
# 阅读阶段
answer_draft: Optional[str] # 答案草稿
source_references: Optional[List[Dict]] # 引用来源
# 验证阶段
confidence_level: Optional[ConfidenceLevel] # 置信度
fact_check_details: Optional[List[Dict]] # 逐条验证结果
needs_human_review: bool # 是否需要人工审核
# 最终输出
final_answer: Optional[str] # 最终答案
final_references: Optional[List[Dict]] # 最终引用来源
# 元数据
agent_logs: List[Dict[str, Any]] # Agent执行日志
三、完整代码实现
3.1 Router Agent(路由Agent)
职责:理解用户问题的类型,决定去哪个知识库检索。
为什么需要路由?
企业知识库不是一个大杂烩,而是按领域分库:
- 技术文档库(工艺规程、操作手册)
- 人事制度库(报销流程、考勤制度)
- 产品数据库(规格参数、价格信息)
- 合规文档库(安全规范、质量标准)
不同领域的检索策略完全不同。
# agents/router_agent.py
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
import json
class RouterAgent:
"""
路由Agent:识别问题类型,决定检索策略
核心能力:
1. 理解用户问题的真实意图
2. 分类到正确的知识领域
3. 决定检索策略(向量/关键词/SQL)
"""
# 问题类型与检索策略的映射
ROUTING_CONFIG = {
QuestionType.TECHNICAL: {
"collections": ["tech_docs", "process_params", "operation_manuals"],
"strategy": "hybrid", # 向量+关键词+SQL
"top_k": 10
},
QuestionType.HR: {
"collections": ["hr_policies", "benefits", "attendance"],
"strategy": "keyword", # 关键词优先(制度文档用关键词更准)
"top_k": 5
},
QuestionType.PRODUCT: {
"collections": ["product_specs", "price_list", "delivery_info"],
"strategy": "sql", # SQL优先(结构化数据)
"top_k": 5
},
QuestionType.POLICY: {
"collections": ["compliance", "safety", "quality"],
"strategy": "hybrid",
"top_k": 10
},
QuestionType.GENERAL: {
"collections": ["company_intro", "faq"],
"strategy": "vector", # 向量即可
"top_k": 3
}
}
def __init__(self, model_name: str = "qwen3-32b"):
self.llm = ChatOpenAI(
model=model_name,
base_url="http://internal-llm:8000/v1",
api_key="dummy"
)
self.prompt = ChatPromptTemplate.from_messages([
("system", """你是企业知识库的路由器,负责判断用户问题的类型。
问题类型:
- technical: 技术类(工艺参数、操作规范、设备维护、加工公差、CNC、质检标准)
- hr: 人事类(报销、考勤、请假、薪资、社保、招聘、入职)
- product: 产品类(规格、价格、交付周期、库存、型号参数)
- policy: 制度类(合规要求、安全规范、质量标准、审批流程)
- general: 通用类(公司简介、闲聊、其他)
输出JSON格式:
{"type": "technical|hr|product|policy|general", "confidence": 0.0-1.0}
只输出JSON,不要输出其他内容。"""),
("user", "{question}")
])
self.chain = self.prompt | self.llm | StrOutputParser()
def route(self, state: KnowledgeState) -> KnowledgeState:
"""路由用户问题"""
# 调用LLM分类
result = self.chain.invoke({"question": state["user_question"]})
# 解析结果
try:
parsed = json.loads(result)
question_type = QuestionType(parsed["type"])
confidence = parsed.get("confidence", 0.5)
except:
# 解析失败,默认通用类
question_type = QuestionType.GENERAL
confidence = 0.3
# 获取检索配置
routing_config = self.ROUTING_CONFIG.get(question_type, self.ROUTING_CONFIG[QuestionType.GENERAL])
# 更新状态
state["question_type"] = question_type
state["target_collections"] = routing_config["collections"]
# 记录日志
state["agent_logs"].append({
"agent": "Router",
"action": "route",
"question_type": question_type.value,
"confidence": confidence,
"target_collections": routing_config["collections"],
"strategy": routing_config["strategy"]
})
return state
3.2 Retriever Agent(检索Agent)
职责:根据路由结果,从多个数据源检索相关信息。
关键能力:三路检索 + RRF融合排序
# agents/retriever_agent.py
import requests
from qdrant_client import QdrantClient
from elasticsearch import Elasticsearch
class RetrieverAgent:
"""
检索Agent:多策略检索企业知识库
核心能力:
1. 向量检索(Qdrant):语义相似度
2. 关键词检索(Elasticsearch):精确匹配
3. SQL查询(结构化数据):产品参数、价格等
4. RRF融合排序:合并三种检索结果
"""
def __init__(self):
# 向量数据库(Qdrant)
self.qdrant = QdrantClient(host="localhost", port=6333)
# 关键词搜索引擎(Elasticsearch)
self.es = Elasticsearch(["http://localhost:9200"])
# Embedding模型
self.embedding_model = "bge-m3" # 中文场景用bge-m3
def retrieve(self, state: KnowledgeState) -> KnowledgeState:
"""
执行检索
根据Router指定的策略,选择检索方式:
- vector: 只用向量检索
- keyword: 只用关键词检索
- sql: 只用SQL查询
- hybrid: 三路检索 + RRF融合
"""
question = state["user_question"]
collections = state["target_collections"]
question_type = state["question_type"]
# 获取检索策略
routing_config = RouterAgent.ROUTING_CONFIG.get(
question_type,
RouterAgent.ROUTING_CONFIG[QuestionType.GENERAL]
)
strategy = routing_config["strategy"]
top_k = routing_config["top_k"]
vector_results = []
keyword_results = []
sql_results = []
# 1. 向量检索
if strategy in ("vector", "hybrid"):
vector_results = self._vector_search(question, collections, top_k)
# 2. 关键词检索
if strategy in ("keyword", "hybrid"):
keyword_results = self._keyword_search(question, collections, top_k)
# 3. SQL查询
if strategy in ("sql", "hybrid"):
sql_results = self._sql_query(question, collections)
# 4. RRF融合排序
if strategy == "hybrid" and (vector_results or keyword_results or sql_results):
merged = self._rrf_merge(vector_results, keyword_results, sql_results, top_k)
else:
# 单策略,直接取top_k
all_results = vector_results + keyword_results + sql_results
merged = sorted(all_results, key=lambda x: x.get("score", 0), reverse=True)[:top_k]
# 更新状态
state["vector_results"] = vector_results
state["keyword_results"] = keyword_results
state["sql_results"] = sql_results
state["merged_results"] = merged
# 记录日志
state["agent_logs"].append({
"agent": "Retriever",
"action": "retrieve",
"strategy": strategy,
"vector_count": len(vector_results),
"keyword_count": len(keyword_results),
"sql_count": len(sql_results),
"merged_count": len(merged)
})
return state
def _vector_search(self, question: str, collections: List[str], top_k: int) -> List[Dict]:
"""向量检索(Qdrant)"""
results = []
# 生成query embedding
query_embedding = self._get_embedding(question)
for collection in collections:
try:
search_results = self.qdrant.search(
collection_name=collection,
query_vector=query_embedding,
limit=top_k
)
for result in search_results:
results.append({
"content": result.payload.get("content", ""),
"source": result.payload.get("source", ""),
"collection": collection,
"score": result.score,
"search_type": "vector"
})
except Exception as e:
print(f"向量检索失败(集合{collection}): {e}")
return results
def _keyword_search(self, question: str, collections: List[str], top_k: int) -> List[Dict]:
"""关键词检索(Elasticsearch)"""
results = []
for collection in collections:
try:
es_result = self.es.search(
index=collection,
body={
"query": {
"multi_match": {
"query": question,
"fields": ["content", "title", "keywords"],
"type": "best_fields",
"fuzziness": "AUTO"
}
},
"size": top_k,
"_source": ["content", "source", "title"]
}
)
for hit in es_result["hits"]["hits"]:
results.append({
"content": hit["_source"].get("content", ""),
"source": hit["_source"].get("source", ""),
"title": hit["_source"].get("title", ""),
"collection": collection,
"score": hit["_score"],
"search_type": "keyword"
})
except Exception as e:
print(f"关键词检索失败(索引{collection}): {e}")
return results
def _sql_query(self, question: str, collections: List[str]) -> List[Dict]:
"""
SQL查询(结构化数据)
用LLM把自然语言转SQL,再查询数据库
"""
# 产品数据库才有结构化数据
if "product_specs" not in collections and "price_list" not in collections:
return []
# 用LLM生成SQL(简化版,实际需要更完善的SQL生成逻辑)
sql_prompt = f"""根据用户问题生成SQL查询。
数据库表结构:
- products: id, name, category, spec, price, stock, delivery_days
- price_list: id, product_name, unit_price, discount, effective_date
用户问题:{question}
输出格式:只输出SQL语句,不要其他内容。"""
# 调用LLM生成SQL
generated_sql = self._call_llm(sql_prompt)
# 执行SQL(实际项目中连接MySQL/PostgreSQL)
try:
# 这里用模拟数据演示
results = self._execute_sql(generated_sql)
return results
except Exception as e:
print(f"SQL查询失败: {e}")
return []
def _rrf_merge(self, vector_results: List[Dict], keyword_results: List[Dict],
sql_results: List[Dict], top_k: int) -> List[Dict]:
"""
RRF(Reciprocal Rank Fusion)融合排序
核心思想:每种检索方式独立排序,然后根据排名融合
公式:RRF_score(d) = Σ 1/(k + rank_i(d))
其中 k=60 是常用常数
"""
k = 60 # RRF常数
doc_scores = {} # doc_id -> RRF score
doc_data = {} # doc_id -> doc data
# 对每种检索结果计算RRF分数
all_results = [
("vector", vector_results),
("keyword", keyword_results),
("sql", sql_results)
]
for search_type, results in all_results:
for rank, result in enumerate(results, start=1):
# 用content的前100字符作为唯一标识
doc_id = hash(result["content"][:100])
if doc_id not in doc_scores:
doc_scores[doc_id] = 0
doc_data[doc_id] = result
# RRF公式
doc_scores[doc_id] += 1.0 / (k + rank)
# 按RRF分数排序
sorted_docs = sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)
# 返回top_k
merged = []
for doc_id, score in sorted_docs[:top_k]:
result = doc_data[doc_id].copy()
result["rrf_score"] = score
result["search_type"] = "merged_rrf"
merged.append(result)
return merged
def _get_embedding(self, text: str) -> List[float]:
"""生成文本embedding"""
# 实际项目中调用embedding服务
resp = requests.post(
"http://embedding-service:8000/embed",
json={"text": text, "model": self.embedding_model}
)
return resp.json()["embedding"]
def _call_llm(self, prompt: str) -> str:
"""调用LLM(简化版)"""
resp = requests.post(
"http://internal-llm:8000/v1/chat/completions",
json={"model": "qwen3-32b", "messages": [{"role": "user", "content": prompt}]}
)
return resp.json()["choices"][0]["message"]["content"]
def _execute_sql(self, sql: str) -> List[Dict]:
"""执行SQL查询(模拟)"""
# 实际项目中连接MySQL/PostgreSQL
import sqlite3
conn = sqlite3.connect("/tmp/products.db")
cursor = conn.cursor()
try:
cursor.execute(sql)
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
return [
{"content": str(dict(zip(columns, row))), "source": "sql_query", "score": 1.0, "search_type": "sql"}
for row in rows
]
finally:
conn.close()
3.3 Reader Agent(阅读Agent)
职责:精读检索结果,提取答案线索。
关键能力:
- 从长文档中提取与问题相关的关键信息
- 保留引用来源(标注"哪段文字来自哪个文档")
- 处理信息冲突(不同文档说法不同)
# agents/reader_agent.py
class ReaderAgent:
"""
阅读Agent:精读检索结果,提取答案
核心能力:
1. 从长文档中提取关键信息
2. 保留引用来源
3. 处理信息冲突
"""
def __init__(self, model_name: str = "qwen3-32b"):
self.llm = ChatOpenAI(
model=model_name,
base_url="http://internal-llm:8000/v1",
api_key="dummy"
)
self.prompt = ChatPromptTemplate.from_messages([
("system", """你是企业知识库的阅读专家,负责从检索结果中提取答案。
要求:
1. 只根据检索结果回答,不要编造
2. 每个断言必须标注来源(文档名+段落)
3. 如果检索结果中没有相关信息,明确说"信息不足"
4. 如果不同文档说法冲突,列出所有版本
输出格式(JSON):
```json
{{
"answer": "答案内容",
"references": [
{{"claim": "断言1", "source": "文档名", "paragraph": "段落内容", "confidence": "high|medium|low"}}
],
"conflicts": [
{{"claim": "冲突的断言", "versions": ["版本1(来源A)", "版本2(来源B)"]}}
]
}}
```"""),
("user", "问题:{question}\n\n检索结果:\n{search_results}")
])
self.chain = self.prompt | self.llm | StrOutputParser()
def read(self, state: KnowledgeState) -> KnowledgeState:
"""精读检索结果,提取答案"""
# 格式化检索结果
search_results = self._format_results(state["merged_results"])
# 调用LLM
result = self.chain.invoke({
"question": state["user_question"],
"search_results": search_results
})
# 解析结果
try:
parsed = json.loads(result)
answer_draft = parsed["answer"]
references = parsed.get("references", [])
conflicts = parsed.get("conflicts", [])
except:
# 解析失败,直接用原始输出
answer_draft = result
references = []
conflicts = []
# 更新状态
state["answer_draft"] = answer_draft
state["source_references"] = references
# 如果有冲突,标记需要人工审核
state["needs_human_review"] = len(conflicts) > 0
# 记录日志
state["agent_logs"].append({
"agent": "Reader",
"action": "read",
"references_count": len(references),
"conflicts_count": len(conflicts),
"needs_human_review": state["needs_human_review"]
})
return state
def _format_results(self, results: List[Dict]) -> str:
"""格式化检索结果(给LLM阅读)"""
formatted = []
for i, result in enumerate(results, 1):
formatted.append(
f"【结果{i}】来源:{result.get('source', '未知')}\n"
f"内容:{result.get('content', '')[:500]}\n"
f"检索方式:{result.get('search_type', '未知')}\n"
)
return "\n---\n".join(formatted)
3.4 FactChecker Agent(事实验证Agent)
职责:逐条验证答案中的断言,对照原文确认事实性。
这是整个系统最关键的Agent。没有它,答案可能是LLM编造的。
# agents/fact_checker_agent.py
class FactCheckerAgent:
"""
事实验证Agent:验证答案的事实准确性
核心能力:
1. 逐条验证答案中的断言
2. 对照原文确认
3. 标记置信度
4. 发现矛盾
设计理念:
- 宁可说"不确定",也不能编造
- 每个断言必须有原文支撑
- 无来源的断言标记为"uncertain"
"""
def __init__(self, model_name: str = "qwen3-32b"):
self.llm = ChatOpenAI(
model=model_name,
base_url="http://internal-llm:8000/v1",
api_key="dummy"
)
self.prompt = ChatPromptTemplate.from_messages([
("system", """你是企业知识库的事实验证专家。
你的任务:逐条验证答案中的断言,对照原文确认事实性。
验证标准:
- confirmed:原文明确支持该断言(引用原文段落)
- uncertain:原文间接支持,但不够明确
- insufficient:原文没有相关内容,断言可能来自LLM推理
- contradicted:原文内容与断言矛盾
输出格式(JSON):
```json
{{
"overall_confidence": "confirmed|uncertain|insufficient|contradicted",
"details": [
{{
"claim": "断言内容",
"verdict": "confirmed|uncertain|insufficient|contradicted",
"evidence": "支撑的原文段落",
"reason": "判断理由"
}}
],
"recommendation": "可以直接输出|建议人工审核|建议拒绝输出"
}}
重要原则:
-
严格对照原文,不要凭常识判断
-
如果原文没有明确支持,标记为uncertain或insufficient
-
如果有矛盾,标记为contradicted"“”),
(“user”, “问题:{question}\n\n答案草稿:{answer_draft}\n\n原文检索结果:\n{search_results}”)
])self.chain = self.prompt | self.llm | StrOutputParser()def check(self, state: KnowledgeState) -> KnowledgeState:
“”“验证答案事实性”“”
# 格式化检索结果
search_results = self._format_results(state[“merged_results”])# 调用LLM验证 result = self.chain.invoke({ "question": state["user_question"], "answer_draft": state["answer_draft"], "search_results": search_results }) # 解析结果 try: parsed = json.loads(result) overall_confidence = ConfidenceLevel(parsed["overall_confidence"]) details = parsed.get("details", []) recommendation = parsed.get("recommendation", "") except: overall_confidence = ConfidenceLevel.UNCERTAIN details = [] recommendation = "建议人工审核" # 根据验证结果决定是否需要人工审核 if overall_confidence in (ConfidenceLevel.INSUFFICIENT, ConfidenceLevel.CONTRADICTED): state["needs_human_review"] = True # 构建最终答案 if overall_confidence == ConfidenceLevel.CONFIRMED: final_answer = state["answer_draft"] elif overall_confidence == ConfidenceLevel.UNCERTAIN: final_answer = ( state["answer_draft"] + "\n\n⚠️ 以上信息未经完全验证,建议以官方文档为准。" ) elif overall_confidence == ConfidenceLevel.INSUFFICIENT: final_answer = "抱歉,知识库中没有找到足够的信息来回答您的问题。建议咨询相关部门。" else: # CONTRADICTED final_answer = "抱歉,不同文档中的信息存在冲突,建议咨询相关部门确认。" # 更新状态 state["confidence_level"] = overall_confidence state["fact_check_details"] = details state["final_answer"] = final_answer state["final_references"] = state["source_references"] # 记录日志 state["agent_logs"].append({ "agent": "FactChecker", "action": "check", "overall_confidence": overall_confidence.value, "details_count": len(details), "needs_human_review": state["needs_human_review"], "recommendation": recommendation }) return statedef _format_results(self, results: List[Dict]) -> str:
“”“格式化检索结果”“”
formatted = []
for i, result in enumerate(results, 1):
formatted.append(
f"【原文{i}】来源:{result.get(‘source’, ‘未知’)}\n"
f"内容:{result.get(‘content’, ‘’)[:500]}\n"
)
return “\n—\n”.join(formatted)
---
### 3.5 构建StateGraph
```python
# graph_builder.py
from langchain.graphs import StateGraph, START, END
def build_knowledge_graph() -> StateGraph:
"""构建企业知识问答Multi-Agent StateGraph"""
graph = StateGraph(KnowledgeState)
# 添加节点
graph.add_node("router", RouterAgent().route)
graph.add_node("retriever", RetrieverAgent().retrieve)
graph.add_node("reader", ReaderAgent().read)
graph.add_node("fact_checker", FactCheckerAgent().check)
# 添加边
graph.add_edge(START, "router")
graph.add_edge("router", "retriever")
graph.add_edge("retriever", "reader")
graph.add_edge("reader", "fact_checker")
graph.add_edge("fact_checker", END)
return graph.compile()
3.6 运行示例
# main.py
from graph_builder import build_knowledge_graph
def main():
graph = build_knowledge_graph()
# 初始状态
initial_state = KnowledgeState(
user_question="CNC加工中心的日常维护操作规范是什么?",
question_type=None,
target_collections=None,
vector_results=None,
keyword_results=None,
sql_results=None,
merged_results=None,
answer_draft=None,
source_references=None,
confidence_level=None,
fact_check_details=None,
needs_human_review=False,
final_answer=None,
final_references=None,
agent_logs=[]
)
# 运行
final_state = graph.invoke(initial_state)
# 输出结果
print("=" * 60)
print(f"问题:{final_state['user_question']}")
print(f"类型:{final_state['question_type'].value}")
print(f"置信度:{final_state['confidence_level'].value}")
print(f"需要人工审核:{final_state['needs_human_review']}")
print("=" * 60)
print(f"答案:\n{final_state['final_answer']}")
print("=" * 60)
# 打印引用来源
if final_state["final_references"]:
print("引用来源:")
for ref in final_state["final_references"]:
print(f" - {ref.get('source', '未知')}:{ref.get('claim', '')}")
# 打印Agent日志
print("\n=== Agent执行日志 ===")
for log in final_state["agent_logs"]:
print(f" {log['agent']} - {log['action']}")
if __name__ == "__main__":
main()
四、真实项目踩坑记录
坑1:路由器把技术问题路由到人事知识库
现象:
用户问:“CNC机床的日常维护规范?”
Router返回:QuestionType.HR
原因:
"日常维护"这个词在人事制度库里有大量匹配(“日常考勤维护”“日常系统维护”),LLM被误导了。
解决方案:
在Router的prompt里加领域关键词:
("system", """...
判断优先级:
1. 先看有没有领域专有名词(CNC、公差、报销、社保)
2. 如果有专有名词,直接按专有名词分类
3. 如果没有,再按语义分类
领域专有名词:
- 技术类:CNC、加工中心、公差、粗糙度、热处理、淬火、数控、刀具
- 人事类:报销、考勤、请假、薪资、社保、公积金、入职
- 产品类:规格、型号、参数、价格、交付、库存
""")
效果:
路由准确率从70%提升到92%。
坑2:向量检索总是返回"看起来像但其实不是"的结果
现象:
用户问:“零件A的加工公差是多少?”
向量检索返回:“零件B的加工公差是±0.05mm”
A和B是不同零件,但文档内容相似(都是"加工公差"),向量距离很近。
原因:
向量检索只看"语义相似度",不看"实体匹配"。A和B虽然话题相似,但对象不同。
解决方案:
加关键词检索做"实体过滤":
# 在向量检索后,加一层实体过滤
def _entity_filter(self, question: str, results: List[Dict]) -> List[Dict]:
"""实体过滤:确保检索结果与问题中的实体一致"""
# 提取问题中的实体(产品名、零件名等)
entities = self._extract_entities(question)
filtered = []
for result in results:
content = result["content"]
# 检查检索结果是否包含问题中的实体
entity_match = any(e in content for e in entities)
if entity_match:
filtered.append(result)
# 如果过滤后为空,返回原始结果(宁可多返回,不能漏掉)
return filtered if filtered else results
效果:
实体相关的检索准确率从55%提升到88%。
坑3:RRF融合排序的权重不好调
现象:
向量检索返回了正确结果(排名第1),但关键词检索返回了错误结果(排名第1),RRF融合后错误结果排在了前面。
原因:
RRF是"民主投票",每种检索方式权重相同。但有些场景下,某种检索方式明显更靠谱。
解决方案:
加权RRF:
def _weighted_rrf_merge(self, vector_results, keyword_results, sql_results,
top_k, weights=None):
"""
加权RRF融合排序
weights: {"vector": 0.5, "keyword": 0.3, "sql": 0.2}
"""
if weights is None:
weights = {"vector": 0.4, "keyword": 0.4, "sql": 0.2}
k = 60
doc_scores = {}
doc_data = {}
for search_type, results, weight in [
("vector", vector_results, weights["vector"]),
("keyword", keyword_results, weights["keyword"]),
("sql", sql_results, weights["sql"])
]:
for rank, result in enumerate(results, start=1):
doc_id = hash(result["content"][:100])
if doc_id not in doc_scores:
doc_scores[doc_id] = 0
doc_data[doc_id] = result
doc_scores[doc_id] += weight / (k + rank)
sorted_docs = sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)
return [doc_data[doc_id] for doc_id, _ in sorted_docs[:top_k]]
权重调优经验:
- 技术文档:向量0.5,关键词0.3,SQL0.2(技术文档语义更重要)
- 人事制度:向量0.2,关键词0.7,SQL0.1(制度文档关键词匹配更准)
- 产品参数:向量0.2,关键词0.2,SQL0.6(结构化数据SQL最准)
坑4:FactChecker太严格,把正确答案也标记为uncertain
现象:
答案"零件A的加工公差是±0.01mm"是正确的,原文也明确写了,但FactChecker标记为uncertain。
原因:
FactChecker的prompt里强调了"严格对照原文",导致LLM过于保守。
解决方案:
调整FactChecker的判断标准:
判断标准:
- confirmed:原文明确包含该断言(不需要逐字匹配,语义一致即可)
- uncertain:原文间接支持(需要推理才能得出断言)
- insufficient:原文完全没有相关内容
- contradicted:原文明确否定该断言
注意:不要过度严格。如果原文明确写了"公差±0.01mm",
答案说"公差是±0.01mm",这是confirmed,不是uncertain。
效果:
confirmed率从30%提升到65%(同时没有增加false positive)。
坑5:长文档的检索结果太长,LLM处理不了
现象:
某个操作手册有200页,检索返回了3个长段落(每个5000字),总共15000字。
LLM的上下文窗口装不下。
解决方案:
在检索阶段加"段落切片":
def _chunk_results(self, results: List[Dict], max_total_chars: int = 8000) -> List[Dict]:
"""截断检索结果,控制总长度"""
total_chars = 0
chunked = []
for result in results:
content = result["content"]
if total_chars + len(content) > max_total_chars:
# 截断到剩余空间
remaining = max_total_chars - total_chars
if remaining > 200: # 至少保留200字
result_copy = result.copy()
result_copy["content"] = content[:remaining] + "..."
chunked.append(result_copy)
break
else:
chunked.append(result)
total_chars += len(content)
return chunked
效果:
LLM输入长度从15000字降到8000字以内,响应速度提升3倍。
坑6-8:简述
坑6:Elasticsearch和Qdrant的搜索结果格式不统一
- 解决:在Retriever Agent里统一输出格式(content/source/score/search_type)
坑7:用户问的问题太模糊(“那个东西怎么弄?”)
- 解决:Router检测到模糊问题时,返回追问(“您指的是哪个产品/流程?”)
坑8:知识库更新后,向量索引没及时更新
- 解决:加一个"增量索引"定时任务(每小时检查新文档,自动embedding+入库)
五、实测效果
5.1 评估数据集
从企业知识库中抽取500个真实问题+标准答案,分为5个领域。
5.2 评估结果
| 指标 | 单Agent RAG | Multi-Agent(本文方案) |
|---|---|---|
| 答案准确率 | 62% | 89% |
| 事实一致性 | 55% | 85% |
| 引用来源准确率 | 无 | 92% |
| 幻觉率 | 23% | 4% |
| 平均响应时间 | 3.2秒 | 8.5秒 |
| 需人工审核比例 | 无(无验证机制) | 12% |
关键发现:
- 准确率从62%提升到89%(+27%),主要来自FactChecker的验证
- 幻觉率从23%降到4%(-19%),主要来自FactChecker拦截uncertain/insufficient答案
- 响应时间从3.2秒增加到8.5秒(4个Agent串行),是代价
5.3 优化方向
降低响应时间:
- Router和Retriever可以并行(Router分类的同时开始检索)
- 缓存高频问题的答案(相似度>0.95直接返回缓存)
提高准确率:
- 加入"用户反馈闭环"(用户点踩后,自动收集bad case)
- 定期更新知识库(增量索引)
六、总结
企业知识问答的核心不是"搜得快",而是"答得准"。
核心经验:
- 路由先行:不同领域用不同检索策略,比一刀切好得多
- 多策略检索:向量+关键词+SQL三路融合,比单路检索准27%
- 事实验证是底线:FactChecker把幻觉率从23%降到4%
- 宁可说不知道,不能编造:这是企业知识库的红线
- 检索结果要控制长度:LLM处理不了15000字的输入
给想搭建企业知识库Agent的公司的建议:
- 不要一上来就搞4个Agent,先从Router+Retriever+Reader开始
- FactChecker是必须的,没有它你不知道AI在编什么
- 知识库质量比算法重要:文档结构化、段落清晰、关键词完整
- 持续运营比初始搭建重要:增量索引+bad case收集+用户反馈
和第一篇文章(AI编程Multi-Agent)的区别:
- 编程Agent关注"代码质量+测试覆盖"
- 知识Agent关注"答案准确+事实验证"
- 编程Agent可以容错(测试不过就改)
- 知识Agent不能容错(编造答案比不回答更危险)
参考资料:
- Qdrant向量数据库文档(https://qdrant.tech/documentation/)
- Elasticsearch全文检索文档(https://www.elastic.co/guide/)
- RRF融合排序论文(《Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods》)
- 某制造业企业知识库项目实战经验(2025-2026年实施)
关于作者:AI小渔村,在渔村里看AI,偶尔捕点新鲜的。数据有出处,代码能运行,欢迎来村里唠嗑。
更多推荐



所有评论(0)