我给公司搭了个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": "可以直接输出|建议人工审核|建议拒绝输出"
}}

重要原则:

  1. 严格对照原文,不要凭常识判断

  2. 如果原文没有明确支持,标记为uncertain或insufficient

  3. 如果有矛盾,标记为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 state
    

    def _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)
  • 定期更新知识库(增量索引)

六、总结

企业知识问答的核心不是"搜得快",而是"答得准"。

核心经验

  1. 路由先行:不同领域用不同检索策略,比一刀切好得多
  2. 多策略检索:向量+关键词+SQL三路融合,比单路检索准27%
  3. 事实验证是底线:FactChecker把幻觉率从23%降到4%
  4. 宁可说不知道,不能编造:这是企业知识库的红线
  5. 检索结果要控制长度:LLM处理不了15000字的输入

给想搭建企业知识库Agent的公司的建议

  1. 不要一上来就搞4个Agent,先从Router+Retriever+Reader开始
  2. FactChecker是必须的,没有它你不知道AI在编什么
  3. 知识库质量比算法重要:文档结构化、段落清晰、关键词完整
  4. 持续运营比初始搭建重要:增量索引+bad case收集+用户反馈

和第一篇文章(AI编程Multi-Agent)的区别

  • 编程Agent关注"代码质量+测试覆盖"
  • 知识Agent关注"答案准确+事实验证"
  • 编程Agent可以容错(测试不过就改)
  • 知识Agent不能容错(编造答案比不回答更危险)

参考资料

  1. Qdrant向量数据库文档(https://qdrant.tech/documentation/)
  2. Elasticsearch全文检索文档(https://www.elastic.co/guide/)
  3. RRF融合排序论文(《Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods》)
  4. 某制造业企业知识库项目实战经验(2025-2026年实施)

关于作者:AI小渔村,在渔村里看AI,偶尔捕点新鲜的。数据有出处,代码能运行,欢迎来村里唠嗑。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐