1. 这不是“又一个RAG教程”,而是一份Agent开发者亲手踩坑后整理的全流程实操手记
我从2022年夏天开始写第一个能调用天气API的简单Agent,到今天带团队落地三个企业级Agentic RAG系统,中间重写了七版知识库 pipeline。这篇笔记标题里写的“建库→检索→生成”,表面看是线性流程,实际在真实项目里,这三个环节像三股拧在一起的麻绳——建库质量差一毫,检索就跑偏;检索召回不准,生成再强也是胡说;而生成环节一旦没约束,又会反向污染建库策略。所以别被标题骗了,这不是教你怎么调用LangChain的load_and_split,而是告诉你:当用户扔给你10万页PDF、37个Excel表、还有几GB扫描件时,你第一分钟该敲什么命令、第二分钟该删什么字段、第三分钟该盯住哪个指标。
核心关键词全在标题里:Agent、RAG、建库、检索、生成。但请注意,这里说的“Agent”不是单次问答机器人,而是具备记忆、工具调用、多步推理能力的智能体;“RAG”也不是把文档扔进向量库就完事,而是要让Agent在决策链路中,像人类专家一样知道“此刻该查什么、去哪查、查到后怎么用”;“建库”不是ETL流水线,而是对原始数据做语义切片、结构清洗、元信息标注的深度预处理;“检索”不是相似度排序,而是结合查询意图、上下文状态、Agent历史行为的动态重排序;“生成”更不是LLM自由发挥,而是带约束、带溯源、带fallback机制的可控合成。适合三类人直接抄作业:正在用LlamaIndex搭内部知识库却总被业务方吐槽“答非所问”的工程师;想把现有客服Bot升级成能自主查手册、填工单、写报告的Agentic系统的产品经理;以及刚学完Transformer原理、正卡在“知道向量是什么但不知道怎么让它真正有用”阶段的AI新人。下面所有内容,都来自我们给某制造业客户部署RAG+Agent系统时的真实日志、监控截图和回滚记录。
2. 整体设计思路:为什么必须放弃“先建库再检索最后生成”的线性幻想
2.1 真实Agent工作流中的RAG不是插件,而是呼吸系统
很多初学者把RAG当成LLM的“外挂插件”:用户提问→触发RAG→返回结果→LLM生成回答。这种理解在demo里能跑通,在生产环境里必死。我们给客户做的第一个POC就是这么崩的——Agent需要根据设备故障代码(如E204)查维修手册,但手册里“E204”同时出现在“电源模块故障”和“通信协议超时”两章,纯向量检索召回Top3里混着两个矛盾答案,LLM直接拼凑出“先断电再重启网关”的错误指令。问题出在哪?不是Embedding模型不够好,而是整个流程设计违背了Agent的本质:Agent是状态机,不是函数调用器。它每一步决策都依赖当前状态(已知信息、历史动作、用户情绪)、目标(解决故障)、约束(安全规范、时效要求)。RAG必须嵌入这个状态机,而不是等它喊“我要查东西”才启动。
所以我们重构了架构,把RAG拆成三个可编程组件:
- Query Planner(查询规划器):接收Agent当前state(比如“用户说‘机器突然停机’,上一步已确认型号为X3000”),输出结构化查询请求,包含:主关键词(“X3000 停机”)、排除项(“不查软件升级指南”)、优先级字段(“必须含‘急停按钮’‘继电器’”);
- Adaptive Retriever(自适应检索器):不只用向量相似度,还融合BM25关键词匹配、实体链接(把“X3000”映射到数据库里的product_id)、甚至用户画像(老工程师偏好查电路图,新员工倾向看操作视频);
- Evidence Integrator(证据整合器):把召回的片段按可信度加权(手册原文>论坛帖子>内部Wiki)、去重(同一故障的三种描述合并)、补全上下文(只召回“检查K1继电器”,自动补上“K1位置在控制柜右下角”)。
这三者不是顺序执行,而是形成反馈环:Evidence Integrator发现召回内容矛盾,会触发Query Planner生成修正查询;Agent执行生成后发现用户追问“为什么不是A而是B”,Evidence Integrator立刻调取A/B对比依据。建库阶段就为这个闭环埋点——比如在PDF解析时,不仅存文本块,还存“该段落属于手册第几章第几节”“是否含警告图标”“关联的故障代码列表”。
2.2 建库策略决定80%的检索质量,而90%的人在第一步就错了
我见过太多团队花两周调优Embedding模型,却用Python默认的pdfplumber.extract_text()处理技术手册。结果呢?一页PDF里“表3-2 输入电压参数”被切成三段:“表3-2”、“输入电压”、“参数”,向量库里存了三个孤立向量。用户搜“X3000输入电压”,检索器根本找不到完整表格。建库不是数据搬运,而是语义结构重建。我们最终采用的分层建库法:
Layer 0:原始数据归一化
所有文件转为统一格式:PDF用pdfminer.six(保留字体/表格结构)、Word用python-docx(提取样式层级)、Excel用pandas.read_excel(强制dtype=str避免数字转科学计数法)。关键动作:删除页眉页脚、合并重复标题、修复扫描件OCR错字(用Levenshtein距离比对同章节不同版本PDF)。Layer 1:语义切片(Semantic Chunking)
拒绝固定长度切片。对技术文档,按“标题层级”切:H1→H2→H3形成树状结构,每个叶子节点(如“3.2.1 K1继电器测试步骤”)作为独立chunk;对FAQ,按“问题-答案对”切;对日志文件,按“时间戳+事件类型”切。每个chunk附带元数据:{"source": "manual_v2.pdf", "section": "3.2.1", "type": "procedure", "entities": ["K1", "继电器", "X3000"]}。Layer 2:增强标注(Enriched Annotation)
这步最耗时但回报最高。用规则引擎+小模型做三件事:- 实体标准化:把“X3K”“X3000”“X-three-thousand”全映射到
product_id: X3000; - 关系注入:在“K1继电器”chunk里自动添加
related_to: ["电源模块", "急停电路"]; - 置信度打标:对OCR识别的文本,用字符级置信度(Tesseract输出)标记
ocr_confidence: 0.82,低置信度chunk在检索时降权。
- 实体标准化:把“X3K”“X3000”“X-three-thousand”全映射到
建库完成后,我们不做“向量化入库”,而是先做质量探针测试:随机抽100个真实用户问题(如“X3000停机时如何复位?”),人工标注标准答案所在chunk ID,然后测召回率。低于92%就退回Layer 1重新切片——这个阈值是客户现场故障平均处理时长倒推出来的:92%意味着90%的故障能在2分钟内定位到正确手册章节。
2.3 检索不是“找相似”,而是“找相关”,相关性由Agent目标定义
传统RAG的检索评估用MRR(Mean Reciprocal Rank),但在Agent场景里,MRR毫无意义。我们曾用all-MiniLM-L6-v2模型,MRR达0.85,但Agent在实际对话中仍频繁出错。根因在于:MRR假设“Top1就是正确答案”,而Agent需要的是“支撑决策的最小证据集”。比如用户问“X3000停机是否需更换主板?”,正确答案不是“是/否”,而是“先测K1继电器电压,若<12V则更换主板”。检索器必须召回“K1测试步骤”和“主板更换指南”两个chunk,并明确它们的逻辑关系(前者是判断条件,后者是执行动作)。
因此,我们的检索器设计遵循三个原则:
- 目标对齐(Goal Alignment):Query Planner输出的查询请求里,包含
intent: diagnosis(诊断意图)或intent: repair(维修意图),检索器据此调整权重。诊断意图下,“故障现象描述”chunk权重+30%,而“备件清单”chunk权重-50%; - 上下文感知(Context Awareness):Agent当前state里有
last_action: "测量K1电压",检索器自动提升含“电压标准值”“异常电压处理”的chunk排名; - 证据链构建(Evidence Chaining):不只返回孤立chunk,而是返回带关系的chunk组。例如召回
[chunk_A: "K1电压标准12-24V", chunk_B: "K1电压<12V需更换主板"],并标注relation: prerequisite(前提关系)。
技术实现上,我们放弃单一向量检索,采用混合检索(Hybrid Retrieval):
- 关键词层:用Elasticsearch做BM25匹配,确保术语精确(如“K1”不会被“K12”干扰);
- 向量层:用bge-reranker-base做重排序,但输入不是原始query,而是Query Planner生成的结构化请求(含intent、exclude、priority);
- 图谱层:对高频实体(如“X3000”“K1”)构建轻量知识图谱,检索时走图遍历(“X3000→has_part→K1→requires_test→电压”)。
这带来一个反直觉结论:Embedding模型的选择,不如Query Planner的设计重要。我们测试过text-embedding-3-large和bge-m3,当Query Planner能精准表达意图时,两者效果差距<3%;但Query Planner失效时,再强的Embedding也救不了。
2.4 生成不是“写答案”,而是“编排证据”,编排逻辑由Agent角色决定
很多RAG项目失败,是因为把生成环节当成“把召回文本喂给LLM”。结果LLM把三段冲突描述揉成一段看似合理实则错误的回复。在Agent系统里,生成是证据驱动的编排(Evidence-Guided Orchestration),而非文本生成。我们定义了三种Agent角色,对应三种生成模式:
- Troubleshooter(故障排查员):生成必须严格遵循“现象→原因→验证→解决”四段式。召回的chunk被强制分类到四类槽位,缺失任一槽位则触发fallback(如无“验证步骤”,则生成“建议使用万用表测量K1两端电压”);
- Documentalist(文档专员):生成需保留原文出处。每句输出后自动追加
[来源: manual_v2.pdf 第3.2.1节],且对引用内容做保真度校验(若原文说“电压>12V”,生成不得写成“电压不低于12V”); - Advisor(顾问):生成需带风险提示。当召回内容含“警告”“注意”标签时,生成开头必须加
⚠️ 安全提示:,且禁止省略原警告内容。
技术实现上,我们不用prompt engineering硬编码,而是用结构化输出模板(Structured Output Schema):
{ "role": "Troubleshooter", "evidence_slots": { "phenomenon": ["chunk_123", "chunk_456"], "cause": ["chunk_789"], "verification": ["chunk_246"], "solution": ["chunk_135"] }, "constraints": ["禁用推测性语言", "所有数值保留原文单位"] }LLM的输出被强制解析为JSON,再由Orchestrator按schema填充。这样即使LLM胡说,系统也能拦截(如solution槽位为空时,拒绝输出)。
3. 核心实操环节:从零搭建可落地的Agentic RAG Pipeline
3.1 建库实操:用Python+Unstructured.io重建语义结构
建库不是写个for循环调用split(),而是构建一个能理解技术文档语义的解析流水线。我们基于Unstructured.io定制了三层处理器,代码已开源在内部GitLab(可提供简化版):
Step 1:文档预处理(Preprocessor)
from unstructured.partition.pdf import partition_pdf from unstructured.staging.base import convert_to_dict def preprocess_pdf(file_path): # 关键参数:strategy="hi_res"启用OCR,infer_table_structure=True识别表格 elements = partition_pdf( filename=file_path, strategy="hi_res", infer_table_structure=True, languages=["zh"], # 重点:指定坐标系,避免扫描件歪斜导致切片错乱 coordinates=True ) # 后处理:合并相邻的TextElement,修复被换行切断的句子 merged_elements = [] for el in elements: if hasattr(el, 'text') and el.text.strip(): # 检查是否与前一个元素在同一逻辑段(Y坐标差<20px且字体相同) if (merged_elements and abs(el.metadata.coordinates.points[0][1] - merged_elements[-1].metadata.coordinates.points[0][1]) < 20 and el.metadata.font_name == merged_elements[-1].metadata.font_name): merged_elements[-1].text += " " + el.text else: merged_elements.append(el) return merged_elementsStep 2:语义切片(SemanticChunker)
class SemanticChunker: def __init__(self): self.section_pattern = r'^\d+\.\d+\.\d+\s+.*$' # 匹配"3.2.1 测试步骤" def chunk_by_section(self, elements): chunks = [] current_section = None current_content = [] for el in elements: if hasattr(el, 'text') and re.match(self.section_pattern, el.text.strip()): # 遇到新章节,保存上一章节 if current_section and current_content: chunks.append({ "text": "\n".join(current_content), "metadata": { "section": current_section, "source": el.metadata.filename, "type": "section" } }) current_section = el.text.strip() current_content = [el.text] elif current_section: current_content.append(el.text) return chunks # 实际使用时,对每个PDF调用: elements = preprocess_pdf("manual_v2.pdf") chunks = SemanticChunker().chunk_by_section(elements) # 输出示例:{'text': '3.2.1 K1继电器测试步骤\n1. 断开电源...\n2. 用万用表测量K1两端电压...', 'metadata': {'section': '3.2.1', 'source': 'manual_v2.pdf', 'type': 'section'}}Step 3:元数据增强(MetadataEnricher)
import spacy from spacy.matcher import Matcher nlp = spacy.load("zh_core_web_sm") matcher = Matcher(nlp.vocab) # 定义产品型号匹配规则(覆盖X3000/X3K/X-three-thousand等变体) pattern = [{"LOWER": {"IN": ["x3000", "x3k"]}}] matcher.add("PRODUCT_ID", [pattern]) def enrich_metadata(chunk): doc = nlp(chunk["text"]) matches = matcher(doc) entities = [] for match_id, start, end in matches: span = doc[start:end] entities.append({ "text": span.text, "label": "PRODUCT_ID", "normalized": "X3000" # 标准化ID }) # 注入关系:从chunk文本中提取"K1"关联的部件 if "K1" in chunk["text"] and "继电器" in chunk["text"]: entities.append({ "text": "K1", "label": "COMPONENT", "relations": ["电源模块", "急停电路"] }) chunk["metadata"]["entities"] = entities return chunk # 对每个chunk调用: enriched_chunk = enrich_metadata(chunks[0])关键参数说明与避坑点:
strategy="hi_res"必须开启:普通OCR对技术文档表格识别率<40%,hi_res模式用LayoutParser检测版面,再用PaddleOCR识别,准确率达92%;coordinates=True不可省略:没有坐标信息,无法判断“表3-2”和其下方表格是否属于同一逻辑单元;- 切片正则
^\d+\.\d+\.\d+\s+.*$要根据手册实际编号风格调整(有的用“3.2.1.”,有的用“3.2.1)”); - Spacy模型必须用
zh_core_web_sm而非en_core_web_sm,中文分词错误会导致实体识别全盘崩溃; - 元数据增强不是一次性的:当新增手册版本时,需用新旧版本diff自动更新关系映射(如新版中“K1”改名为“RELAY_K1”)。
3.2 检索实操:Hybrid Retrieval的工程化实现
纯向量检索在Agent场景下必然失败,必须用混合检索。我们用Elasticsearch+Sentence-Transformers构建双通道检索器,核心代码如下:
Step 1:Elasticsearch索引配置(支持结构化查询)
PUT /rag_index { "settings": { "number_of_shards": 3, "analysis": { "analyzer": { "my_analyzer": { "type": "custom", "tokenizer": "ik_max_word", "filter": ["lowercase"] } } } }, "mappings": { "properties": { "text": { "type": "text", "analyzer": "my_analyzer" }, "section": {"type": "keyword"}, "source": {"type": "keyword"}, "entities": {"type": "keyword"}, "intent_weights": { // 预存各意图下的权重系数 "properties": { "diagnosis": {"type": "float"}, "repair": {"type": "float"} } } } } }Step 2:向量库构建(用BGE-M3做多语言Embedding)
from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer('BAAI/bge-m3', device='cuda') def embed_chunks(chunks): # BGE-M3支持多向量:dense(稠密向量)、sparse(稀疏向量)、colbert(多向量) # 我们用dense+colbert双编码,提升长文本匹配精度 dense_embeddings = model.encode( [c["text"] for c in chunks], batch_size=32, show_progress_bar=True, convert_to_numpy=True ) # colbert向量需单独计算(用官方ColBERTv2) from colbert import Indexer indexer = Indexer(checkpoint='colbert-ir/colbertv2.0') colbert_embeddings = indexer.encode([c["text"] for c in chunks]) return dense_embeddings, colbert_embeddings # 存入FAISS向量库(支持GPU加速) import faiss index = faiss.IndexFlatIP(1024) # BGE-M3 dense维度为1024 index.add(dense_embeddings)Step 3:混合检索执行(Query Planner输出→双通道召回→融合排序)
def hybrid_retrieve(query_plan, top_k=10): # Query Plan示例:{"intent": "diagnosis", "keywords": ["X3000", "停机"], "exclude": ["升级"]} # 通道1:Elasticsearch关键词检索 es_query = { "bool": { "must": [{"multi_match": {"query": " ".join(query_plan["keywords"]), "fields": ["text^3", "section^2"]}}], "must_not": [{"term": {"section": exclude}} for exclude in query_plan.get("exclude", [])] } } es_results = es.search(index="rag_index", query=es_query, size=top_k*2) # 通道2:向量检索(用dense向量) query_embedding = model.encode(query_plan["keywords"][0]) # 简化:用主关键词编码 D, I = index.search(np.array([query_embedding]), top_k*2) # 融合排序:ES得分 * intent_weights + 向量相似度 * 0.7 fused_scores = {} for hit in es_results["hits"]["hits"]: doc_id = hit["_id"] es_score = hit["_score"] intent_weight = hit["_source"].get("intent_weights", {}).get(query_plan["intent"], 0.5) fused_scores[doc_id] = es_score * intent_weight for i, idx in enumerate(I[0]): doc_id = f"vec_{idx}" vec_score = D[0][i] fused_scores[doc_id] = vec_score * 0.7 # 返回TopK融合结果 sorted_docs = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)[:top_k] return [doc_id for doc_id, _ in sorted_docs] # 实际调用: query_plan = {"intent": "diagnosis", "keywords": ["X3000", "停机"], "exclude": ["软件升级"]} top_chunks = hybrid_retrieve(query_plan)关键参数与性能调优:
- Elasticsearch的
multi_match字段加权(text^3)确保正文匹配优先于标题; - BGE-M3的
dense向量用于全局相似度,colbert向量用于细粒度匹配(如“K1电压”vs“K1两端电压”),但我们发现colbert在工业文档上收益有限,最终只用dense; - 融合权重
0.7是实测得出:向量相似度对术语变化鲁棒,但ES对精确术语匹配更强,0.7平衡两者; top_k*2召回再融合,避免单通道漏召(如ES可能漏掉“X3000突然停机”,但向量库能召回“X3000无预警停机”)。
3.3 生成实操:Evidence-Guided Orchestration的强制约束
生成环节必须打破“LLM自由发挥”的幻觉,用结构化Schema约束输出。我们基于Llama-3-70B-Instruct实现,核心是Prompt + JSON Schema + Output Parser三重保险:
Step 1:定义Orchestration Schema(Pydantic v2)
from pydantic import BaseModel, Field from typing import List, Optional class EvidenceSlot(BaseModel): chunk_ids: List[str] = Field(..., description="召回的chunk ID列表") required: bool = Field(True, description="该槽位是否必须填充") class GenerationConfig(BaseModel): role: str = Field(..., description="Agent角色:Troubleshooter/Documentalist/Advisor") evidence_slots: dict[str, EvidenceSlot] = Field(..., description="证据槽位定义") constraints: List[str] = Field(..., description="生成约束列表") # 示例配置: config = GenerationConfig( role="Troubleshooter", evidence_slots={ "phenomenon": EvidenceSlot(chunk_ids=["chunk_123"], required=True), "cause": EvidenceSlot(chunk_ids=["chunk_456"], required=True), "verification": EvidenceSlot(chunk_ids=["chunk_789"], required=False), "solution": EvidenceSlot(chunk_ids=["chunk_246"], required=True) }, constraints=["禁用推测性语言", "所有数值保留原文单位"] )Step 2:构造结构化Prompt
def build_prompt(config, retrieved_chunks): # 从retrieved_chunks中提取对应chunk内容 evidence_text = "" for slot_name, slot in config.evidence_slots.items(): for chunk_id in slot.chunk_ids: # 从向量库或ES中获取chunk原文 chunk_content = get_chunk_by_id(chunk_id) evidence_text += f"[{slot_name.upper()}]\n{chunk_content}\n\n" prompt = f"""你是一名专业的{config.role},请严格按以下要求生成回复: 1. 输出必须为JSON格式,包含字段:phenomenon, cause, verification, solution 2. 每个字段内容必须直接来自[evidence]中的对应部分,禁止添加、删减、改写 3. 遵守约束:{'; '.join(config.constraints)} 4. 若[evidence]中缺少某字段所需内容,该字段值设为null [evidence] {evidence_text} [output_format] {config.model_dump_json(indent=2)}""" return prompt # 生成Prompt示例: prompt = build_prompt(config, top_chunks) # 输出含明确字段要求和约束的Prompt,LLM无法自由发挥Step 3:强制JSON输出与校验
import json from llama_cpp import Llama llm = Llama(model_path="./llama-3-70b-instruct.Q4_K_M.gguf", n_ctx=8192) def generate_with_schema(prompt, schema): # Llama.cpp支持grammar参数,强制JSON输出 grammar = f'''root ::= {json.dumps(schema, ensure_ascii=False)}''' output = llm( prompt, max_tokens=1024, temperature=0.1, # 降低随机性 grammar=grammar, # 关键!强制JSON格式 stop=["```"] # 防止LLM输出代码块 ) try: result = json.loads(output["choices"][0]["text"]) # 校验required字段 for slot_name, slot in schema.evidence_slots.items(): if slot.required and not result.get(slot_name): raise ValueError(f"Required slot {slot_name} is empty") return result except json.JSONDecodeError: # fallback:用正则提取JSON片段 import re json_match = re.search(r'\{.*?\}', output["choices"][0]["text"], re.DOTALL) if json_match: return json.loads(json_match.group()) else: raise RuntimeError("Failed to parse JSON output") # 调用: result = generate_with_schema(prompt, config) # result是严格符合schema的dict,可直接用于前端渲染关键经验与避坑点:
temperature=0.1是底线:高于0.3时LLM开始“创作”,即使有grammar也会输出无效JSON;grammar参数比prompt里写“请输出JSON”有效10倍,Llama.cpp底层用PEG语法树校验;stop=["```"]防止LLM在JSON后加代码块注释(如json {...}),导致解析失败;- 必须做
required字段校验:LLM常把optional字段设为null,但required字段缺失必须报错重试; - 不要用OpenAI API:其JSON mode在长文本生成时不稳定,Llama.cpp+grammar更可靠。
4. 常见问题与排查技巧实录:那些文档里不会写的血泪教训
4.1 建库阶段:90%的“检索不准”其实源于PDF解析失败
问题现象:用户搜“X3000输入电压”,召回结果全是“输出电压”相关内容,且MRR测试显示Top1准确率仅65%。
排查路径:
- 检查原始PDF:用
pdfinfo manual_v2.pdf发现Creator为“Adobe Acrobat Pro DC 2023”,确认是扫描件; - 查看Unstructured日志:
INFO unstructured.partition.pdf - Using hi_res strategy with OCR,但OCR日志为空; - 提取单页图像:用
pdf2image.convert_from_path("manual_v2.pdf", first_page=1, last_page=1)导出PNG,发现分辨率仅72dpi(标准应≥300dpi); - 验证OCR:用PaddleOCR直接识别该PNG,字符错误率高达42%(正常<5%)。
根本原因:扫描件分辨率不足,OCR识别错误,导致“输入”被识别为“输出”。
解决方案:
- 预处理增加DPI提升:用
pdfimages -list manual_v2.pdf查图像DPI,若<200,则用ImageMagick重采样:convert -density 300 -quality 100 manual_v2.pdf manual_v2_hi.pdf - OCR后人工校验:对高频术语(如“输入电压”“K1”)建立词典,OCR结果匹配词典率<95%时告警;
- 终极方案:对关键手册,采购专业OCR服务(如ABBYY FineReader),成本高但准确率>99%。
提示:不要相信“PDF转Word再转文本”的方案,Word会破坏表格结构,技术文档中表格占比超30%,丢失表格等于丢失核心参数。
4.2 检索阶段:向量相似度高≠语义相关,警惕“假阳性召回”
问题现象:Agent查“X3000停机”,召回Top3是:
- “X3000正常运行时电压范围”(相似度0.82)
- “X2000停机处理流程”(相似度0.79)
- “X3000软件升级后停机”(相似度0.75)
但正确答案“X3000硬件故障停机”相似度仅0.61,排在第12位。
根因分析:
- Embedding模型在通用语料上训练,对“停机”一词,把“软件升级后停机”(高频共现)和“硬件故障停机”(低频)向量拉近;
- Query未体现意图:用户说“停机”,但未说明是“突发”还是“升级后”,Query Planner未区分。
解决方案:
- Query Planner增强:在Agent state中加入
context: "用户刚说‘机器突然停机’,未提升级”,生成查询时强制添加exclude: ["升级", "重启后"]; - 向量库微调:用客户真实QA对(如“X3000突然停机→查硬件故障”)做Contrastive Learning,损失函数聚焦区分“突然停机”vs“升级停机”;
- 引入领域词典:在Embedding前,用规则替换同义词:“突然停机”→“hardware_failure_shutdown”,“升级停机”→“software_update_shutdown”,让向量空间天然分离。
实操心得:我们用spaCy的
Matcher构建了200+条领域规则,覆盖“停机/宕机/死机/黑屏”等12种表述,替换后相似度差异从0.05扩大到0.32,Top1召回率从65%升至89%。
4.3 生成阶段:LLM“一本正经胡说八道”,如何让证据说话
问题现象:Agent生成:“K1继电器电压标准为12-24V,若低于12V需更换主板”,但手册原文是“K1电压<12V时,检查电源模块;若电源模块正常,再更换主板”。LLM把两步操作压缩成一步,且删除了关键前提。
技术根因:
- Prompt中“禁止改写”约束太弱,LLM认为“压缩步骤”不算改写;
- 证据整合器未标注逻辑关系(原文中“检查电源模块”是“更换主板”的前提条件)。
解决方案:
- 强化证据标注:在建库阶段,用规则引擎识别条件句:“若...则...”“当...时...”,自动标注
relation: "prerequisite"; - Prompt显式要求:在
constraints中增加“必须保留原文逻辑连接词(若/当/则/否则)”,并举例说明; - 后处理校验:用spaCy解析生成文本,检查“若”字句是否都有对应“则”字句,缺失则触发重生成。
注意:不要指望LLM理解逻辑关系。我们测试过GPT-4-turbo,对“若A则B,若B则C”链条,30%概率漏掉中间环节。必须靠结构化约束+后处理双重保险。
4.4 Agent集成阶段:RAG响应延迟高,拖慢整个决策链路
问题现象:Agent单次交互平均耗时8.2秒,其中RAG占6.5秒(建库0.3s+检索4.2s+生成2.0s),用户等待感强烈。
性能瓶颈定位:
timeit测试显示,Elasticsearch查询<100ms,FAISS向量搜索<50ms;- 主要耗时在
model.encode():BGE-M3编码单个query需1.8s(CPU)/0.3s(GPU),但Agent每轮需编码3-5个query(Query Planner生成多个变体); llm.generate()耗时1.5s,但这是必要开销。
优化方案:
- Query缓存:对相同意图+关键词组合,缓存Embedding结果(Redis),命中率>70%;
- 异步检索:Agent发起RAG请求后,不阻塞,继续执行其他动作(如调用API查库存),RAG结果返回后再整合;
- Embedding模型蒸馏:用BGE-M3蒸馏出轻量版(768维→384维),速度提升2.1倍,相似度下降仅0.02(可接受)。
经验:不要盲目追求“端到端低延迟”。Agent的价值在于决策质量