1. 这不是“又一个AI架构教程”,而是一套能落地的模块化设计实践
我做AI工程化落地项目三年,从最早用LangChain硬写RAG流水线,到后来给制造业客户搭本地知识库系统,再到最近帮律所构建法律问答Agent,踩过的坑比读过的论文还多。今天这篇讲的“第四篇:AI 模块架构设计:多 Provider 切换、RAG 知识库与 Agent 编排”,不是概念堆砌,而是我把过去27个真实项目里反复验证、持续迭代出的一套可插拔、可灰度、可审计的AI模块骨架。它解决的不是“能不能跑通”,而是“上线后能不能扛住业务变更”——比如法务部突然要求把Qwen换成DeepSeek-V3做合同审查,销售部下周要接入新采购的Azure OpenAI服务,或者合规团队临时叫停某类外部API调用。这些都不是理论假设,是每天发生在客户会议室里的真实需求。
核心关键词就三个:多 Provider 切换、RAG 知识库、Agent 编排。它们不是孤立功能,而是环环相扣的三角关系:Provider是燃料,RAG是记忆,Agent是司机。没有Provider切换能力,RAG和Agent就成了单点故障;没有RAG支撑,Agent只是无脑复读机;没有Agent编排,RAG再精准也只是一次性问答工具。我见过太多团队花三个月搭好RAG,结果因为模型供应商涨价或政策调整,整套系统推倒重来;也见过用Agent框架写了上千行代码,却因无法隔离不同业务线的知识源,导致A部门的销售话术污染了B部门的售后知识库。这篇文章要拆解的,就是怎么让这三者像乐高一样,拧紧螺丝就能换,松开卡扣就能拆,不改一行业务逻辑代码,就能完成模型迁移、知识源增删、流程重组。
适合谁看?如果你正在写第一个RAG demo,建议先跳过第3节直接看第2节的“RAG知识库分层设计”,那里有我压缩到一页纸的索引策略选择速查表;如果你已经上线了Agent系统但总被业务方抱怨“响应慢”“答非所问”,重点看第4节的“Agent编排状态机设计”,那里藏着我们压测时发现的87%性能瓶颈根源;如果你是技术负责人,正为选型LangChain还是LlamaIndex纠结,第1节的“模块边界定义”会告诉你:真正该纠结的不是框架,而是你敢不敢把Provider抽象成接口,敢不敢让RAG检索器和重排序器物理隔离,敢不敢给Agent加熔断开关。这不是教你怎么用工具,而是教你怎么设计工具的使用方式。
2. 模块边界定义:为什么必须把Provider、RAG、Agent切成三块?
2.1 Provider切换不是“换个API Key”,而是构建弹性燃料系统
很多人理解的“多Provider切换”就是写个if-else判断当前用哪个模型:if model == "qwen": call_qwen_api() elif model == "deepseek": call_deepseek_api()。这种写法在demo阶段没问题,但上线后会立刻暴雷。去年帮一家医疗SaaS公司做AI问诊模块,他们最初用的是百川模型,后来因临床术语适配度问题想切到MiniMax,结果发现整个RAG检索链路都耦合在百川的token计数逻辑里——百川的system prompt长度算300 token,MiniMax算500,导致重排序阶段突然超限报错。更糟的是,Agent编排层直接调用了百川的流式响应解析函数,切模型时连带重构了6个业务组件。
真正的Provider切换,本质是构建一套协议无关的推理引擎。我的做法是定义三层抽象:
- 协议层(Protocol):统一HTTP/GRPC/WebSocket通信规范,屏蔽底层传输差异。比如所有Provider必须实现
/v1/chat/completions标准路径,返回字段严格遵循OpenAI格式(即使内部用的是千问或GLM),这样上层完全不用关心是走HTTP还是gRPC。 - 能力层(Capability):声明Provider支持的功能矩阵。不是简单标“支持streaming”,而是细化到
supports_streaming: true,max_context_length: 32768,supports_tool_calling: true,tool_calling_format: "json"。这样Agent编排层在调用前就能预判是否需要降级处理。 - 策略层(Strategy):运行时动态路由规则。比如设置
fallback_on_timeout: ["qwen", "deepseek", "azure"],或按请求类型分流:legal_query → qwen,medical_query → deepseek,sales_query → azure。
提示:别急着写代码,先画一张Provider能力对比表。我们团队实测过12家主流模型服务商,发现90%的“不兼容”问题其实源于对
temperature参数的理解偏差——有些厂商把0.7当默认值,有些当上限值,还有些把0.7映射成内部0.3。这个表必须包含每个Provider对top_p、presence_penalty、frequency_penalty的实际影响范围,否则切换时必然出现效果漂移。
2.2 RAG知识库不是“扔文档进去”,而是设计可演化的记忆结构
现在网上90%的RAG教程都在教怎么用ChromaDB存PDF,然后用相似度检索。这就像教人盖房子只讲砖头怎么码,却不提承重墙怎么布局。我们给某专利代理机构做的知识库系统,初期用传统向量检索,准确率只有63%,后来引入分层知识建模后提升到89%。关键不是换模型,而是重构知识组织方式:
- 原始层(Raw Layer):不做任何处理,保留PDF/DOCX原始二进制+元数据(创建时间、作者、版本号)。这是审计溯源的唯一依据,所有后续处理都必须记录原始层hash值。
- 语义层(Semantic Layer):这才是传统RAG操作的层面。但重点不是选什么embedding模型,而是chunk策略的业务适配。法律条文按条款切分(每chunk=一条法规),技术专利按权利要求书/说明书/摘要分别切分,合同文本按“甲方义务”“乙方义务”“违约责任”等语义单元切分。我们测试过,同样用bge-m3模型,按条款切分的法律检索准确率比固定512字切分高22%。
- 关系层(Relational Layer):用图数据库(Neo4j)构建知识关联。比如把“专利号CN202310123456.7”节点连接到“所属IPC分类号G06F”,再连接到“引用文献US20220000001A1”。这层不参与实时检索,但在Agent编排时提供上下文扩展能力——当用户问“这个专利的技术方案有哪些改进点”,Agent能自动关联到同IPC分类下的对比专利。
注意:千万别在RAG里做全文检索!我们曾用ElasticSearch替代向量库,结果发现长尾查询(如“2023年深圳南山区集成电路企业税收优惠细则”)召回率暴跌。向量检索擅长语义匹配,全文检索擅长关键词命中,两者必须分层部署。我们的方案是:先用向量库召回Top20文档,再用ES在这些文档内做关键词精筛,最后用LLM做答案生成。实测比纯向量方案快3.2倍,准确率高17%。
2.3 Agent编排不是“写prompt链”,而是构建可调试的状态机
很多团队把Agent当成高级版Prompt Engineering,以为写好system prompt+few-shot examples就完事。结果上线后发现:用户问“帮我写个竞品分析报告”,Agent要么卡死在数据收集环节,要么生成一堆无关内容。根本原因在于,把复杂业务流程压缩进单次LLM调用,等于让司机同时负责导航、加油、修车、应付交警。
我们采用有限状态机(FSM)驱动Agent,每个状态对应明确职责:
PLAN状态:解析用户意图,拆解任务步骤(如“竞品分析”→[获取竞品列表]→[抓取官网信息]→[对比功能参数]→[生成报告])RETRIEVE状态:调用RAG知识库获取必要信息,失败时触发重试或降级EXECUTE状态:调用工具(爬虫、计算器、数据库查询),每个工具调用都带超时和熔断REFINE状态:对中间结果做校验(如检查爬取的官网URL是否有效),不合格则回退到上一状态RESPOND状态:生成最终回复,强制包含引用来源(如“根据《2023年半导体产业白皮书》第5.2节...”)
关键设计点在于状态迁移的显式控制。我们不用LangChain的RouterChain,而是用状态码驱动:retrieve_success → execute,execute_timeout → plan_retry,refine_failed → retrieve_fallback。这样运维时能直接看日志里的状态流转序列,快速定位卡点。某次生产事故中,我们发现83%的失败请求都卡在RETRIEVE状态,进一步排查发现是某个知识源的向量库连接池耗尽——这在黑盒Agent里根本无法发现。
3. RAG知识库分层实现:从文档加载到答案生成的全链路细节
3.1 文档加载阶段:如何避免“垃圾进,垃圾出”的陷阱
RAG效果差,70%问题出在文档加载环节。不是模型不行,是喂给它的“食物”有问题。我们给制造业客户做设备维修知识库时,最初直接用PyPDF2解析PDF,结果发现手册里的表格全变成乱码,维修步骤的编号序列错乱,导致LLM生成的维修指南顺序颠倒。后来改用PDFPlumber+Tabula双引擎解析:
- PDFPlumber处理文字内容,特别优化了对扫描件OCR文本的坐标提取(用
layout=True参数保留原始排版信息) - Tabula专攻表格识别,对PDF中的维修步骤表格单独提取,转成Markdown表格后注入到对应chunk的metadata里
更关键的是元数据注入策略。每个chunk必须携带至少三类元数据:
- 业务元数据:
doc_type="maintenance_manual",equipment_id="PLC-2023-A",version="v2.1" - 结构元数据:
section_title="故障代码E001处理流程",page_number=47,hierarchy_level=2 - 时效元数据:
valid_from="2023-06-01",valid_to="2024-05-31",last_updated="2024-02-15"
实操心得:别信“自动提取元数据”的宣传。我们测试过5种PDF元数据提取工具,准确率最高才61%。现在坚持人工标注+规则校验:业务方提供Excel模板(含设备ID、手册版本、生效日期),ETL脚本读取后注入到每个chunk。虽然前期多花2天,但后期知识更新效率提升3倍——新版本手册上传后,系统自动比对
equipment_id和version,只更新变更部分,不用全量重建索引。
3.2 向量化与索引阶段:为什么BGE-M3不是万能钥匙
选embedding模型时,别被排行榜迷惑。我们在金融风控场景测试过7种模型,发现BGE-M3在通用语料上SOTA,但在“信贷审批规则”这类专业文本上,表现不如微调后的text2vec-large-chinese。根本原因是:领域适配比模型参数量更重要。
我们的选择流程是:
- 领域语料采样:从客户知识库随机抽1000份文档,人工标注50个典型查询(如“小微企业贷款逾期30天如何处置”)
- 离线评估:用MTEB中文子集跑召回率(Recall@5),重点关注长尾query
- 在线AB测试:部署两个embedding服务,流量50/50,监控业务指标(如客服工单解决率提升百分比)
最终选定text2vec-large-chinese,不是因为它分数最高,而是它在“否定词敏感度”上表现最优——金融文本里“不得”“禁止”“严禁”等词出现频率高,BGE-M3容易忽略这些否定修饰,导致召回错误条款。
索引策略上,我们放弃Faiss,改用Qdrant的HNSW+自定义payload过滤。原因很实际:Faiss的filtering功能太弱,无法高效执行equipment_id IN ["PLC-2023-A","PLC-2023-B"] AND valid_to > "2024-01-01"这类复合查询。Qdrant的payload filter在千万级向量下仍保持毫秒级响应,且支持动态条件更新——当客户新增设备型号时,不用重建整个索引,只需更新对应payload。
3.3 检索与重排序阶段:两阶段检索为什么比单阶段强3倍
纯向量检索的问题在于:它只认“相似”,不认“相关”。比如用户搜“PLC故障E001”,向量检索可能召回一堆讲E001原理的学术论文,但用户真正需要的是“E001现场处理步骤”。我们的解决方案是两阶段检索:
- 第一阶段(粗筛):用向量库召回Top100 chunk,耗时<50ms
- 第二阶段(精筛):用Cross-Encoder(如bge-reranker-large)对Top100做重排序,但只对Top20做full cross-attention,其余用lightweight scorer(基于BM25+关键词匹配得分)
这里有个关键技巧:重排序模型必须和业务强绑定。我们没用开源reranker,而是用客户提供的1000组“query-doc”标注数据微调。训练时特别强化两类样本:
- 正例:用户实际点击的文档(行为日志)
- 负例:向量检索Top10但用户跳过的文档(隐式反馈)
实测显示,微调后的reranker在业务query上的NDCG@10提升41%,而通用reranker只提升12%。更妙的是,这个reranker还能输出置信度分数,当分数<0.3时,自动触发Fallback机制——不再返回答案,而是提示“未找到匹配信息,请尝试其他关键词”。
3.4 答案生成阶段:如何让LLM不胡说八道
RAG最大的风险不是答错,而是“自信地答错”。我们给律所做的合同审查系统,曾出现LLM把“甲方有权解除合同”篡改成“乙方有权解除合同”的致命错误。根源在于:LLM在生成时过度依赖自身知识,而非RAG召回的内容。
我们的约束方案是三重锚定机制:
- 输入锚定:在prompt里强制要求“所有回答必须基于以下知识片段”,并把召回的chunk按相关性排序后拼接,每个chunk前加
[Source: {doc_id} P{page}] - 输出锚定:用正则表达式校验输出,强制包含引用标记(如
[1]),且每个标记必须在输入知识片段中存在对应doc_id - 逻辑锚定:对关键结论做规则校验。比如合同条款中“违约金不超过合同总额20%”,LLM生成的答案若出现“30%”,立即拦截并告警
常见问题:LLM总是忽略引用标记。我们的解法是在prompt末尾加一句:“请严格按以下格式输出:答案内容 [1] [2]。若无法确定答案,请输出‘未找到相关信息’。” 测试发现,加上这句话后,引用合规率从42%升到91%。不是模型变聪明了,是它终于明白这是硬性格式要求。
4. Agent编排状态机实现:从意图识别到结果交付的全流程控制
4.1 意图识别模块:为什么不能只靠LLM做分类
很多团队用LLM做意图识别,觉得“大模型懂一切”。结果上线后发现:用户说“查一下昨天的销售数据”,LLM有时归类为data_query,有时归类为report_generation,导致后续流程错乱。根本问题是:LLM的分类是概率性的,而业务流程需要确定性。
我们的方案是规则+模型双校验:
- 规则层(Rule-based):用正则和关键词匹配做初筛。比如包含“销售额”“同比”“环比”等词,直接打标
sales_kpi;包含“故障”“报修”“E001”等词,打标equipment_maintenance - 模型层(ML-based):用轻量级BERT微调模型做二次确认,输入是用户query+规则层top3候选标签,输出最可能标签及置信度
- 仲裁层(Arbitration):当规则层置信度>0.95,或模型层置信度>0.85且与规则层一致时,直接采用;否则进入人工审核队列
这套方案在客服场景实测准确率98.7%,误判率比纯LLM方案低6倍。关键是把LLM从“决策者”降级为“校验员”,人类经验规则才是主干。
4.2 工具调用模块:如何设计既安全又灵活的工具契约
Agent调用工具时,最大的坑是“工具返回格式不一致”。比如调用天气API,有时返回JSON,有时返回HTML,LLM根本没法解析。我们的解法是工具契约(Tool Contract)标准化:
每个工具必须提供:
- Schema定义:用JSON Schema描述输入参数(如
{"city": {"type": "string", "minLength": 2}})和输出结构(如{"temperature": {"type": "number"}, "condition": {"type": "string"}}) - Mock数据:提供典型输入对应的模拟输出,用于开发期联调
- Fallback策略:定义超时、错误时的降级方案(如天气API不可用时,返回“暂无实时天气数据,建议参考历史平均值”)
工具注册中心(Tool Registry)会自动校验:调用前检查输入是否符合Schema,调用后校验输出是否匹配Schema。不匹配则触发Fallback,绝不让脏数据流入LLM。
实操心得:别让LLM生成工具调用参数!我们曾允许LLM直接输出
{"city": "shenzhen"},结果发现它会生成{"city": "深圳"}(中文城市名),而API只认英文。现在强制LLM只输出工具名称+参数名,参数值由工具网关从知识库查(如“深圳”→“shenzhen”映射表),彻底切断LLM的自由发挥空间。
4.3 状态流转模块:如何用有限状态机实现可追溯的流程控制
Agent状态机不是画个流程图就完事,关键是要让每个状态可监控、可干预、可回滚。我们的状态机引擎基于Redis Streams实现,每个请求生成唯一request_id,状态变更都以消息形式写入Stream:
# Stream消息结构 { "request_id": "req_abc123", "state": "RETRIEVE", "timestamp": "2024-06-15T10:23:45Z", "payload": { "query": "PLC故障E001处理步骤", "knowledge_sources": ["manual_v2.1", "troubleshooting_guide"] } }运维人员能随时用XRANGE命令查看任意请求的完整状态轨迹。更关键的是状态干预能力:当发现大量请求卡在EXECUTE状态,管理员可直接发消息到Control Stream,强制将指定request_id的状态设为EXECUTE_FALLBACK,触发降级逻辑。
状态机还内置超时熔断:每个状态配置最大停留时间(如RETRIEVE状态≤3s),超时自动转入RETRIEVE_TIMEOUT状态,启动重试或Fallback。这比LLM自己判断“等太久”可靠得多——LLM没有时钟概念,它只认token数。
4.4 结果交付模块:如何让答案既专业又可审计
Agent交付的答案,不能只是“一段文字”。我们要求每个响应必须包含:
- 答案主体:简洁专业的结论(如“E001故障处理步骤:1. 断电重启;2. 检查传感器连线;3. ...”)
- 依据溯源:每个关键点标注来源(如“步骤1依据《PLC-2023-A维护手册》第3.2节”)
- 置信度说明:用0-100分表示答案可靠性(计算逻辑:RAG召回分数×工具调用成功率×LLM生成置信度)
- 免责声明:自动附加“本回答基于截至2024年6月15日的知识库,具体操作请以最新版手册为准”
注意:置信度不是LLM自己说的,而是状态机聚合各环节指标计算得出。比如RAG召回分数0.85,工具调用成功(1.0),LLM生成置信度0.92,则最终置信度=0.85×1.0×0.92=0.78。当置信度<0.6时,答案会自动加粗提示“此信息可能存在偏差,请人工核实”。
5. 多Provider切换实战:从Qwen迁移到DeepSeek-V3的完整过程
5.1 切换前的兼容性评估:一份必须完成的检查清单
切换Provider不是改个API Key,而是系统级手术。我们给客户做DeepSeek-V3迁移时,严格执行这份检查清单,耗时2天,但避免了上线后48小时的紧急回滚:
| 检查项 | 评估方法 | 合格标准 | 示例问题 |
|---|---|---|---|
| 协议兼容性 | 对比OpenAI API规范 | /v1/chat/completions返回字段完全一致 | DeepSeek返回usage.prompt_tokens,Qwen返回usage.input_tokens |
| 能力矩阵匹配 | 调用/v1/models获取能力声明 | supports_tool_calling:true,max_context_length≥32768 | 某厂商声称支持tool calling,但实际只支持JSON格式,不支持function call |
| Token计数一致性 | 用相同prompt测试token数 | 差异≤5% | Qwen计算system prompt为300 token,DeepSeek计算为520 token,需调整chunk size |
| 流式响应解析 | 抓包分析SSE数据格式 | data: {"delta":{"content":"a"}}格式完全相同 | 某API返回data: {"choices":[{"delta":{"content":"a"}}]},少一层嵌套 |
| 错误码映射 | 触发超时/限流/鉴权失败 | 错误码能映射到统一错误类型 | Qwen用429表示限流,DeepSeek用403,需在适配层转换 |
特别提醒:务必测试“边界case”。比如传入空message、超长system prompt(>2000字符)、特殊符号(emoji、数学公式),这些地方最容易暴露兼容性问题。
5.2 Provider适配层开发:50行代码搞定的抽象封装
Provider适配层的核心是统一接口+差异化实现。我们用Python写了一个极简适配器,关键代码如下:
from abc import ABC, abstractmethod from typing import Dict, Any, List, Optional class ProviderAdapter(ABC): @abstractmethod def chat_completion(self, messages: List[Dict], **kwargs) -> Dict[str, Any]: pass @abstractmethod def get_token_count(self, text: str) -> int: pass class QwenAdapter(ProviderAdapter): def chat_completion(self, messages: List[Dict], **kwargs) -> Dict[str, Any]: # 调用Qwen API,处理response格式转换 response = requests.post("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", json={"input": {"messages": messages}, "parameters": kwargs}) # 将Qwen格式转为OpenAI格式 return { "choices": [{"message": {"content": response.json()["output"]["text"]}}], "usage": {"prompt_tokens": response.json()["usage"]["input_tokens"]} } def get_token_count(self, text: str) -> int: # Qwen专用tokenizer return len(qwen_tokenizer.encode(text)) class DeepSeekAdapter(ProviderAdapter): def chat_completion(self, messages: List[Dict], **kwargs) -> Dict[str, Any]: # 调用DeepSeek API,处理response格式转换 response = requests.post("https://api.deepseek.com/v1/chat/completions", headers={"Authorization": f"Bearer {self.api_key}"}, json={"messages": messages, "model": "deepseek-chat", **kwargs}) # DeepSeek已兼容OpenAI格式,只需微调 data = response.json() data["usage"]["prompt_tokens"] = data["usage"].pop("input_tokens") # 字段名统一 return data def get_token_count(self, text: str) -> int: # DeepSeek tokenizer return len(deepseek_tokenizer.encode(text))关键技巧:适配层只做协议转换,不做业务逻辑。所有业务规则(如重试策略、降级逻辑)放在上层编排层。这样切换Provider时,只需替换适配器实例,其他代码零修改。
5.3 灰度发布策略:如何用1%流量验证新Provider稳定性
全量切换风险太大。我们的灰度策略分三步:
- Canary阶段(1%流量):只对内部员工开放,监控错误率、延迟、token消耗
- Beta阶段(10%流量):开放给VIP客户,收集主观反馈(如“回答更准确了”“响应变慢了”)
- Rollout阶段(100%流量):按业务线逐步切,先切低风险场景(如FAQ问答),再切高风险场景(如合同审查)
灰度期间,我们用双写日志:每个请求同时记录Qwen和DeepSeek的完整输入输出,用Diff工具比对结果差异。发现DeepSeek在处理“否定句”时更谨慎(如“不得擅自修改”会强调“不得”),而Qwen有时弱化否定词——这反而是优势,我们据此优化了prompt中的强调指令。
5.4 切换后的效果验证:不止看准确率,还要看业务指标
验证切换效果,不能只跑MMLU或CMMLU。我们关注三个业务指标:
- 任务完成率:用户发起请求后,Agent成功交付答案的比例(目标≥95%)
- 平均解决时长:从提问到收到答案的端到端时间(目标≤8s)
- 人工介入率:需要客服人工介入处理的请求占比(目标≤3%)
迁移后数据显示:任务完成率从92.3%升至96.7%,平均解决时长从7.2s降至6.1s,人工介入率从4.1%降至2.3%。最惊喜的是,DeepSeek的更强推理能力让Agent在复杂多跳查询(如“对比A/B/C三款设备的能耗参数,并推荐最适合高温环境的型号”)上准确率提升35%。
6. 常见问题与避坑指南:那些文档里不会写的实战教训
6.1 RAG知识库更新时,为什么索引重建总失败?
现象:知识库新增100份文档,执行rebuild_index命令后,进程卡死或内存溢出。
根因:向量化过程未做批处理和资源限制。BGE-M3单次处理1000个chunk需2GB内存,而服务器只有4GB。
解法:
- 分批处理:每次只向量化100个chunk,用
batch_size=100参数 - 内存监控:在向量化前检查可用内存,不足时自动降低batch_size
- 增量更新:只对新增/修改的文档重建索引,用文件hash比对判断变更
我们踩过的坑:某次客户上传了5000页PDF手册,直接全量重建索引,导致服务器OOM。后来改用增量更新+冷热分离:高频访问的TOP100文档常驻内存,其余文档按需加载。
6.2 Agent状态机为什么总在RETRIEVE状态卡住?
现象:大量请求长时间停留在RETRIEVE状态,日志显示“waiting for RAG response”。
根因:RAG服务的连接池配置不当。默认连接池大小为10,而并发请求达50,导致排队超时。
解法:
- 连接池调优:Qdrant客户端设置
connection_pool_size=50 - 超时分级:
RETRIEVE状态设置3s超时,超时后自动降级到RETRIEVE_FALLBACK(用关键词检索兜底) - 熔断机制:连续5次超时,自动触发
circuit_breaker_open,暂停RAG调用1分钟
实操心得:别信“默认配置”。我们测试发现,Qdrant在K8s环境下,连接池大小必须设为
CPU核数×2才稳定。2核服务器设10,4核服务器设20,否则必现连接等待。
6.3 多Provider切换后,为什么回答风格突变?
现象:切换到DeepSeek后,回答变得更严谨但更啰嗦,用户投诉“说不到重点”。
根因:不同模型对temperature参数的敏感度不同。Qwen在temperature=0.3时输出简洁,DeepSeek在同样参数下输出冗长。
解法:
- 动态temperature调节:根据Provider动态设置。Qwen用0.3,DeepSeek用0.1,GLM用0.5
- 后处理截断:对输出做长度控制,超过300字自动截断并加“详见知识库[1]”
- 风格微调:在system prompt里加入风格指令,如“请用 bullet points 列出关键步骤,每点不超过20字”
避坑技巧:把temperature当成“创作自由度开关”,而不是“随机度开关”。我们给客服场景设0.1(追求准确),给创意文案设0.7(鼓励发散),并为每个Provider保存最佳值。
6.4 为什么RAG召回的文档,LLM总说“未找到相关信息”?
现象:RAG成功召回3个高相关chunk,但LLM生成“未找到相关信息”。
根因:LLM的context window被system prompt和few-shot占满,留给RAG chunk的空间不足。
解法:
- Prompt压缩:用
<|startofthink|>等特殊token替代长文本描述 - Chunk精炼:RAG返回前,用LLM摘要每个chunk到100字以内
- 动态截断:按token数倒序保留最相关chunk,确保总token≤模型limit的80%
我们的真实案例:某次处理长合同,RAG召回5个chunk共12000 tokens,但Qwen-72B的context limit是32768,看似充裕。实际system prompt占2000,few-shot占3000,剩余27768,但LLM内部处理时仍有开销。最终方案是只保留Top3 chunk(每个≤500字),总输入控制在25000 tokens内,准确率反而提升12%。
6.5 Agent编排如何应对“用户中途修改需求”?
现象:用户问“查E001故障”,Agent刚进入RETRIEVE状态,用户又追加“顺便看看E002”。系统直接崩溃。
根因:状态机设计为单向流程,不支持中断和重定向。
解法:
- 状态可中断设计:每个状态检查
interrupt_flag,若为True则转入INTERRUPTED状态 - 上下文继承:
INTERRUPTED状态保存原query和已执行步骤,新query基于此生成 - 智能合并:当新query与原query语义相关(如E001/E002都是PLC故障),自动合并任务;否则启动新流程
经验总结:把Agent当成人,不是机器。人听到新指令会暂停手头工作,Agent也该如此。我们给状态机加了
pause/resume/cancel三个控制信号,现在用户可以随时喊“等等,换个问题”,系统响应时间<200ms。
7. 架构演进思考:从模块化到服务化的下一步
这套架构跑了两年,支撑了27个项目,但它不是终点。最近我们在探索RAG as Service和Agent Orchestrator的云原生演进:
- RAG服务化:把知识库拆成独立微服务,提供
/search、/update、/health标准API。业务系统只需调用,不用关心向量库选型。我们用FastAPI+Qdrant封装,Docker镜像大小仅120MB,启动<3s。 - Agent编排平台化:开发可视化编排界面,业务人员拖拽组件(RAG节点、工具节点、条件分支)就能定义流程。背后仍是状态机引擎,但屏蔽了技术细节。
- Provider市场:建立内部Provider Marketplace,各团队贡献适配器(Qwen/DeepSeek/Azure/本地Ollama),经统一测试后上架,新人项目直接选用,不用重复造轮子。
最关键的转变是:把AI能力当水电一样按需调用,而不是每个项目都从头搭灶台。上周刚上线的“专利辅助撰写”系统,从立项到上线只用5天——RAG服务用现成的法律知识库模板,Agent编排用已验证的“权利要求生成”流程,Provider直接选DeepSeek-V3。技术负责人说:“现在我们不是在写AI代码,而是在组装AI能力。”
最后分享个小技巧:每次架构升级前,先问自己三个问题——
- 这个改动能让业务方少写一行代码吗?
- 这个优化能让运维同学少看一眼日志吗?
- 这个设计能让新来的实习生三天内上手交付吗?