做物流智能客服这个项目之前,我在Spring Boot里已经写了三年的订单、运单、报表,LLM那套东西在我看来也就是圈子里在炒新概念。直到产品经理把一个需求拍在我桌上:客服机器人要能查物流轨迹、能回答面单规则和理赔条款、还能记住客户上次说过什么。我一开始的想法是搞三个独立服务去堆:一个知识库问答、一个会话记录、一堆OpenAI Function Calling,然后在Controller里手工编排。后来发现Spring AI里三个东西——RAG、记忆、工具调用——设计出来就是为了一体化干这件事的,它们能串成一个Agent运行时的统一对话流。这篇文章把我实际搭建这套物流智能客服系统的过程完整整理出来,重点不是某个API怎么调,而是这三样东西为什么必须同时存在、合并到一起时各自负责什么、顺序怎么编排。适合正在用Java、想给Spring Boot工程塞进AI能力、又不想把各家供应商SDK散落到业务代码里的团队参考,也适合对RAG只停留在概念阶段、想看看真实落地长什么样的同学。
1. 为什么非要把这三样绑在一起:物流客服的痛点拆解
1.1 客服一天里到底在处理什么
我蹲过一阵子客服工单系统,把物流客服的真实工作抽出来,其实就三类问题:
- 实时信息类:“我的件到哪了”“运费多少钱”“今天能送到吗”。这些数据不在任何文档里,在TMS、OMS、财务系统里,需要动态查询。
- 规则知识类:“面单上要写什么”“锂电池能不能走航空”“拒收后运费谁承担”“理赔要几个工作日”。这些内容写在操作手册、合同条款、公告通知里。
- 连续性类:客户昨天投诉过丢件,上午才说好“下午再送一次”,下午又来问进度。客服如果记不住,客户就得重复两遍,体验直接崩。
这三类问题恰好对应三种技术:工具调用、RAG、记忆。一开始我也想过只做其中一样,但数据说话:我拿过去三个月2000条真实客服会话做了统计,大约34%是实时信息查询,48%是规则知识问答,剩下的18%都要依赖历史上下文。你只做RAG,最多解决一半问题;只做工具调用,规则类问题模型就开始一本正经地瞎编;只做记忆,什么东西都答不了,只是显得“有礼貌”。
1.2 只做单一能力的系统为什么会被骂
我在预研阶段写过三个最小原型,专门踩这些坑。
只接RAG不接工具的demo,客户问“我的件现在停在哪”,系统从知识库里检索出一句“包裹已从XX转运中心发出”,看着合理,其实是另一单的文档片段。因为轨迹是实时数据,静态库里根本没有,模型就把相似的内容当答案吐了出来。
只接工具不接RAG的demo,客户问“运单上有错别字能不能快递员帮我改”,系统没法回答,因为改面单规则不在工具返回里,模型自由发挥就会说出“请联系快递员修改”这种违规结论。实际上物流公司对改面单有严格流程,乱答会扯出责任问题。
最让我尴尬的是没有记忆的版本。一个客户早上问过理赔材料,下午同一会话里问“我已上传材料,下一步呢”,系统完全不记得上午发生过什么,重新开始问“请问您要咨询理赔吗”。这种机器人上了线,客户能忍住不骂人已经算素质高。
1.3 目标与验收口径
所以这个项目从立项开始,目标就不是“做一个能聊天的机器人”,而是“用一套对话运行时,把三类问题在一个入口里解决掉”。验收口径也很直白:人工客服介入率能不能降下来,一次性解决率能不能升上去,以及错误回答(尤其是涉及理赔、时效承诺的)能不能控制在安全范围。后面所有设计和压测,都是围绕这三个指标在转。
2. 工程底座:把Spring AI接进Spring Boot
2.1 选型:Spring AI还是LangChain4j
团队里当时有两条路线争议,用LangChain4j的人觉得生态靠近LangChain,文档多;我的判断是:物流系统里已经有大量Spring Boot服务、注册中心、配置中心,团队没有Python背景,维护一套非Spring生态的Agent框架成本会很高。Spring AI是Spring官方在做的事,和Spring Boot的自动装配、starter机制天生能嵌,模型供应商抽象也够用。
后来我也看了一下Spring AI Alibaba(官方社区里基于通义的那套适配),如果业务跑在阿里云上,替换起来就是改依赖和配置的事。这种可替换性对我们很重要,因为物流公司的模型选型往往不是定死的,今天用OpenAI,明天可能因为合规要求换掉。我不太认同那些把LangChain4j说得一无是处的观点,它有自己的优势,只是在这个Java为主、靠近Spring生态的团队里,Spring AI是更省事的底座。
2.2 依赖和配置:别小看这一步
Maven依赖长这样,我用的是当时比较稳的1.1.x版本,双版本管理:
<dependencyManagement> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencyManagement> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-tika-document-reader</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store</artifactId> </dependency>我这里用PGVector做向量库,因为它直接建在已有的PostgreSQL实例里,运维不用额外引入Milvus。配置上我建议embedding模型和chat模型分开配,别图省事都用同一个。我们的chat走OpenAI兼容接口,embedding走本地Ollama的nomic-embed-text,理由后面讲RAG部分会展开。关键配置大概是这样:
spring: ai: ollama: base-url: http://your-embedding-host:11434 embedding: model: nomic-embed-text pgvector: index-type: hnsw dimension: 768 initialize-schema: true openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2有几个必须注意的细节:PGVector的dimension必须和embedding模型输出维度一致,我用的nomic-embed-text是768维,如果换bge-m3就是1024维,建表后改维度非常痛苦,最好上线前就定死。initialize-schema: true会自动建vector扩展,但生产环境我建议DBA手动执行,避免AI框架在启动时拿到过高权限。还有temperature不要给太高,客服场景需要的是稳定,不是创意。
2.3 把ChatClient收敛成唯一入口
Spring AI的ChatClient是整个系统的核心,类似Spring Boot里的RestTemplate,所有聊天请求都从它过。我封装成了一个Spring Bean,而不是在每个Controller里各自new:
@Configuration public class AicAgentConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory, VectorStore vectorStore) { return builder .defaultAdvisors( new UserProfileAdvisor(userProfileMemory()), // 自定义长期记忆,后面细讲 new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(vectorStore, searchRequestConfig()), new ToolCallAdvisor() ) .defaultTools( new QueryTrackTool(), new CalcFreightTool(), new QueryTimingTool() ) .build(); } }这种写法的好处是,以后新加记忆策略、新加知识库filter、新加工具,都在一个地方改,而不是改散落在Controller里的prompt拼接逻辑。我团队后来维护这套代码的同事最感谢我的就是这一点,不是你多牛逼,而是别人接手时只用看这一个类就能理解全貌。
3. RAG不是“扔PDF进去就能答”:物流文档的知识工程
3.1 拆文档:固定长度切分在物流场景会翻车
RAG第一节就翻车了。我把一份《国内标准快递理赔条款》PDF按500字符切块灌进向量库,然后问“派送延误超过几天可以申请理赔”,系统答非所问。查了检索命中的chunk才发现,条款里“延误理赔时效为X天”这句话被切成了两半,上半截在chunk 17,下半截在chunk 18,embedding之后语义就断了。
这件事让我明白,物流文档拆解不能图省事用固定长度,要按文档结构拆。Spring AI提供了不少splitter,我最后用的是组合策略:
List<Document> pdfDocs = new PagePdfDocumentReader( new FileSystemResource("rules/compensation.pdf") ).get(); MarkdownHeaderTextSplitter headerSplitter = new MarkdownHeaderTextSplitter(); TokenTextSplitter overlapSplitter = TokenTextSplitter.builder() .chunkSize(400) .chunkOverlap(80) .build(); // 策略:先把PDF文本转成Markdown,按标题切出章节,再对小章节做overlap切分 List<Document> chunks = new ArrayList<>(); for (Document doc : pdfDocs) { for (Document mdDoc : headerSplitter.apply(doc.getText())) { chunks.addAll(overlapSplitter.apply(mdDoc)); } }对Excel那种表格型文档,我另外写了一个转换器,把每一行转成一句自然语言描述,比如“华东区域:爆款特惠产品,首重1kg以内8元”,再灌进向量库。直接按行列拆分喂进去,embedding模型根本理解不了列结构,这是本地知识库里表格文档最常见的坑。
3.2 检索命中率上不去,别先怪模型
我在项目里建了一个只有80条问题的评估集,每条问题人工标注“应该命中哪些知识段落”,然后用一个指标看系统是不是真的找到对的东西:hit rate,也就是正确片段是否出现在TopK检索结果里。
第一版结果很难看,hit rate只有0.62,意味着38%的问题根本没检索到正确答案,后面无论模型多聪明都白搭。排查下来有三个原因:
- 同义词缺失:客户问“泡货怎么收费”,文档里写的是“轻泡件计算规则”,向量的距离没那么近,Top5没带上。
- 表格知识没做语义化:禁运品Excel里“电子烟”被归在“电池类危险品”,embedding后语义离得远。
- TopK太小:默认TopK=4,有些正确段落排在第五、第六位。
我的处理是对知识库做了一层“同义词别名”扩充:在原始段落开头加一行“本标准适用于轻泡件、泡货、抛货等表述”,让这个chunk对多种问法都有响应。然后把TopK提到6,在线检索时再配合后置过滤,成本可控。调整完后hit rate到了0.84,我又在检索结果后面接了一层rerank,把候选段落用模型重新打分,最终稳定在0.9左右。
提示:hit rate衡量的是“检索环节有没有捞到正确答案”,不是“回答对不对”。如果你发现AI答错,先查知识有没有被放进用户上下文,再查模型的prompt,别一上来就换大模型。
3.3 QuestionAnswerAdvisor:检索接入对话流
在Spring AI里,RAG通过QuestionAnswerAdvisor接入聊天会话,它做的事情是在真正调用大模型之前,用向量库检索出相关内容,拼到prompt里。我这边的调用方式:
QuestionAnswerAdvisor questionAnswerAdvisor = new QuestionAnswerAdvisor( vectorStore, SearchRequest.builder() .topK(6) .similarityThreshold(0.45) .build(), "回答物流规则问题时应优先引用提供的知识库内容。" + "如果知识库没有直接依据,必须回复'该问题需要人工客服协助确认',不得自行推断。" );这个自定义system prompt非常关键。通用RAG教程会让你说“请根据上下文回答”,但物流客服的合规要求更高,没有依据时必须转人工。比如客户问“寄香水到新加坡需要什么文件”,知识库里没有这条,模型就不该硬编。我们还专门做过红鲱鱼测试:故意问一个知识库里不存在的问题,看系统是不是宁可承认不知道也不瞎说,这个指标比回答多流畅都重要。
3.4 知识更新:给文档加版本号
物流规则更新频率比想象中高,特别是节假日时效调整、燃油附加费变动。我的做法比较简单:给每个文档加上updatedAt元数据,VectorStore写入时用filter只检索指定时间范围内的chunk。Spring AI的SearchRequest支持表达式过滤器:
FilterExpressionBuilder b = new FilterExpressionBuilder(); SearchRequest request = SearchRequest.builder() .filterExpression(b.and( b.gte("updatedAt", versionStamp) ).build()) .build();更省事的方案是直接换collection名做版本隔离:新规则上线时灌新的collection,发布时把系统切过去。缺点是得重灌所有embedding,但物流公司的知识库规模一般也就几千个chunk,重灌成本并不高,换来的是规则版本的确定性。
4. 记忆:短期滚动上下文加长期用户画像
4.1 Spring AI现有的短期记忆机制
Spring AI的ChatMemory接口管理会话历史,MessageChatMemoryAdvisor负责在每次请求时把历史消息注入给模型。核心键是conversationId,客户端每次会话传同一个ID,AI才能把多轮对话串起来。
我在生产里用的不是默认的InMemoryChatMemory。客服系统会水平扩容,请求会落在不同Pod上,本地内存一清空就“失忆”。Spring AI的Redis适配可以解决这个问题:
@Bean public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) { return new RedisChatMemory(redisTemplate); }注意:Redis里存的每条消息都要带角色和时间戳,否则排序会乱。我踩过一次坑,早期版本没有正确处理角色,结果System Prompt被当成用户消息写进历史,下一次调用全部串味。
短期记忆窗口要设上限。我一直用最近10轮对话,不是越多越好。太长的历史会把模型注意力稀释,它开始重复历史里的细节,真正要处理的“查运费”“查轨迹”反而被淹没。短窗口还有个附带的好处是节省token,客服场景量大,省token就是省钱。
4.2 跨对话不忘事:长期记忆就该用“置信度+时间半衰期”
短期记忆解决了单个会话内的连续性,但客户隔一天再来,还是什么都不记得。群里讨论“跨对话记忆”时,有个思路我沿用了下来:把记忆抽象成一条条“事实”,每条事实带一个置信度分数,并且随着时间衰减。这个思路后来看有点像通义那边分享的双网络记忆模型的实践——短期工作记忆解决当前对话,长期记忆库存储跨会话画像,信息在两者之间流转。
我实现得比较简单。客服会话结束后,用一个异步任务让LLM从刚结束的对话里抽取事实,格式类似:
{ "facts": [ { "slot": "delivery_time_preference", "value": "下午18:00后签收", "confidence": 0.9 } ], "sensitive_removed": ["身份证号", "手机号"] }写进长期记忆库(我用Redis做存储,key是userId,value是JSON数组),同时记录写入时的last_used时间。每次用户发起新会话时,扫描该用户所有记忆,按半衰期公式计算当前有效强度:
public double decay(double confidence, Instant lastUsed, int halfLifeDays) { long days = Duration.between(lastUsed, Instant.now()).toDays(); return confidence * Math.pow(0.5, (double) days / halfLifeDays); }我这边把halfLifeDays设为30天。客户两个星期前说过“晚上6点后派送”,这条记忆强度还有0.65左右,应该保留;三个月前的记忆强度已经跌到0.1以下,就别往prompt里塞了。然后写一个自定义的UserProfileAdvisor,在请求真正发给模型之前,把有效记忆拼成一段“过往偏好”放进系统提示词。
这一步把“客户上次说不要放驿站”从一次性的对话记忆,变成了三个月后依然生效的用户画像。上线后客服团队反馈最明显的就是,客户不再需要重复交代“放在门口快递柜就行”,AI自己会问“还是放到丰巢吗”。
4.3 记忆污染:情绪和敏感信息是两把刀
记忆侧我栽过两次,写出来给你们避坑。
第一次是记忆抽取的时候把情绪状态也存进去了。“客户非常生气,说要投诉到底”被我作为一条事实记了下来。结果下一次对话时,AI一上来就道歉、态度卑微,正事没干。原因就是情绪型记忆把整个session的基调带偏了。解决方案:抽取事实时明确只提取客观偏好、身份、业务事实,不提取情绪状态,除非情绪是业务关键信息(比如投诉中)。
第二次是隐私。物流客服对话里天然有身份证号、手机号、地址这类敏感信息。如果不做脱敏,这些信息存进Redis就是定时炸弹。我在抽取环节挂了敏感字段过滤器,正则识别手机号、身份证后直接替换成占位符,记忆库里存的是“手机号已提供”,不是真实的11位数字。这个处理当时被安全同事特批通过,是项目能过审的关键。
5. 工具调用:让模型去“戳”那些接口
5.1 用@Tool定义一组物流工具
Spring AI的工具调用相比自己拼Function Calling协议好处在于,它用注解标注方法就能自动生成模型的tool schema。我定义了一套和物流客服强相关的工具:
@Component public class QueryTrackTool { @Tool(description = "根据运单号查询最新物流轨迹") public String queryTrack( @ToolParam(description = "运单号,一般为10位或12位数字") String trackingNo) { TrackDTO track = trackApi.query(trackingNo); return track.formatForAgent(); } } @Component public class CalcFreightTool { @Tool(description = "计算两个地址之间的物流运费") public String calcFreight( @ToolParam(description = "始发地,格式:省市区,如'上海市浦东新区'") String from, @ToolParam(description = "目的地,格式:省市区,如'浙江省杭州市西湖区'") String to, @ToolParam(description = "重量,单位公斤,数字") double weight, @ToolParam(description = "产品类型,如:标快、特快、经济件") String productType) { return freightApi.quote(from, to, weight, productType); } }工具不是越多越好。第一版我加了12个工具,包括“改地址”“催派件”“生成电子面单”,结果模型选工具经常选错,本该查运费的去调了改地址。后来砍到5个核心工具:查轨迹、算运费、查时效、查网点、查理赔进度,准确率明显回升。模型每次可选的工具越少,它的决策越稳,这是工具设计的第一原则。
5.2 参数校验和返回结构是工具侧的隐形陷阱
模型生成参数不是100%可靠的,跟模型说“start_date是2024-01-01”,它可能给你回“2024年1月1日”。Spring AI的@ToolParam只提供description,不负责做实际校验。我的建议是工具方法内部必须做一遍硬校验:
- 运单号:正则先过一遍,长度、数字规则不对,直接返回“运单号格式不正确,请确认后重试”。
- 地址:先做一次匹配,匹配不到就在返回信息里提示模型“未识别到地址,请让用户补充城市信息”。
- 重量:负数直接抛错,不要让模型自己去想“重量为负怎么办”。
返回内容的结构更要命。工具返回的明文如果是一大段JSON,模型很容易把字段名当成废话重复,或者为了简化而遗漏信息。我把每个工具的结果封装成面向客服话术的紧凑文本,比如:
轨迹: 2024-06-12 10:23 [上海转运中心] 已离开,下一站杭州转运中心 时效: 预计06-13 18:00前到达 最新状态: 运输中这种格式模型几乎不会二次加工出错,而且省token。我之前犯过把完整物流详情HashMap直接toString塞给模型,结果模型开始胡编“派送员张三电话138…”的长篇大论。
5.3 工具不可用时的兜底策略
线上一定会遇到第三方接口超时或者熔断。我的处理是:工具内部捕获异常后,不抛异常,而是返回“轨迹查询服务暂时不可用,请告知客户稍后查询或转人工”。这句话看似简单,实际作用很大——它让模型知道工具确实调了,但没拿到数据,从而不会自己编造轨迹。
如果要更严格,可以在系统提示词里写死:“所有工具返回的信息都是唯一事实来源,工具不可用时必须承认无法查询,禁止猜测运单状态。”我测试过,没有这句提示词的情况下,6次里有1次模型会在工具失败后编一个看似合理的轨迹,这在客服场景是不能接受的。
6. 三件套的编排顺序:Advisor链怎么串
6.1 Advisor机制:大模型调用前后的过滤器
Spring AI把RAG、记忆、工具调用都抽象成了Advisor,你可以想象成Java Web里的Filter链:请求进来,依次经过每个Advisor,每个Advisor可以在大模型调用前改写请求(加记忆、加上下文、补充工具),也可以在大模型返回后处理响应(记录日志、格式化输出)。
我实际跑通的链路是:
ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( new UserProfileAdvisor(), // 1. 注入长期记忆 new MessageChatMemoryAdvisor(), // 2. 注入短期历史 new ToolCallAdvisor(), // 3. 注册工具 new QuestionAnswerAdvisor() // 4. 注入知识库片段 ) .defaultTools(queryTrackTool, calcFreightTool, ...) .build();这个顺序不是随便排的。
- 长期记忆和短期历史必须放最前面,因为它们是“背景信息”,模型理解整个对话需要它们。
- 工具调用放中间,是为了让模型先知道它有几个工具可用,再去决定要不要用。
- RAG放最后,因为知识库检索出来的参考内容是对最终答案影响最大的上下文,越靠后的Advisor生成的内容越容易被模型优先采信。
6.2 工具和知识库打架时,必须说清楚谁说了算
这是整个项目里最微妙的地方。有一次客户问“从广州到北京的经济件多久能到”,知识库里恰好有一份“经济件时效3-5天”的旧时效表,工具实时查询返回的却是“已延误,预计6天”。模型不淡定了,它同时看到两个信息,最后把知识库的3-5天和工具的6天各抄了一半,回答“预计5-6天”。
问题的根源是没有明确知识库和工具的优先级。我在系统提示词里补了一条规则:
实时数据的唯一来源是工具返回结果,知识库只用于回答规则条款类问题。如果工具返回的时效、运费、轨迹与知识库描述不一致,一律以工具返回为准。
补上这条后,类似冲突基本消失。你也可以在RAG的检索语句里把动态数字类信息过滤掉,但提示词层面先讲清楚优先级是成本最低的做法。
6.3 让每次调用都可观测
AI客服上了线,最可怕的是黑盒。客户投诉说“机器人给我承诺今天一定到”,你查日志却发现模型压根没有输出这句话,AI还跟你犟嘴。所以我在每个Advisor前后都打了结构化日志:
- 每次RAG检索命中了哪几个chunk,相似度分数多少
- 每次工具调用传入了什么参数、返回了什么、耗时多少
- 一次完整问答消耗了多少输入token和输出token
Spring AI自带LoggerAdvisor,会让你在日志里看到prompt和response的全量内容,开发期很好用。生产环境我建议自己写一个轻量级Advisor只记录元数据,因为全量prompt日志量非常大,而且可能包含敏感信息,日志平台都撑不住。
6.4 并发和超时:客服场景比想象中残酷
压测的时候我发现了两个性能问题。
第一个是模型API的并发限制。团队用的那个API服务有每分钟调用上限,客服高峰期瞬间涌入几十个并发,直接429。解决方案是给ChatClient调用加了Semaphore限流和队列,宁可让用户多等几秒,也不能让请求直接失败。第二个是超时。一次完整问答链路要经过记忆读取、向量检索、工具调用、大模型生成,任何一个环节慢都拖后腿。我把大模型调用超时设成15秒,工具调用超时设成3秒,整体超时设成20秒。超过20秒直接返回“后台正在处理,请稍后查看”,绝不让用户体验无限转圈。
7. 上线后的实际效果与几个值得记下来的坑
7.1 效果数据
系统上线后运行了一个多月,我们拿到的数据大概是这样的:在内部评估集上,典型知识类问题的回答准确率从第一版的47%提到了81%;转人工率下降了22个百分点;人工客服日常会话里,重复让客户描述问题的情况明显减少,因为记忆真的把跨会话的上下文记住了。
我特别看重的一个数据是“无依据断言率”,也就是模型在知识库没有依据时还强行回答的比例。我们专门做了一个监测报表,用规则扫描回复文本,命中“预计明天到”“一定送达”这类强承诺话术的对话,下降了很多。这在物流咨询里比准确率更重要,因为乱承诺时效是要赔钱的。
7.2 复盘:三个返工最多的坑
第一,记忆抽取不能贪多求全。我最早以为抽取的事实越多越好,结果系统开始记“客户喜欢用蓝色字体”“客户在第3句时语气不好”这种垃圾。后来给抽取的slot做了白名单,只有地址偏好、派送偏好、业务标签(如“老客户”“协议客户”)等业务相关的才允许写入,垃圾记忆骤减。
第二,RAG文档更新慢导致回复过时。有一版运费规则变更后,生产环境还挂着旧知识库,AI按旧的“首重8元”报价。后来我做了个简单的定时同步任务,每次规则变更是自动灌新chunk到新collection,并同步修改配置中心里当前collection的指向。麻雀虽小,但保证“AI读到的一定是最新一版”,这种自动化让我能睡个安稳觉。
第三,不要把模型能力浪费在低价值内容上。有些对话模型根本不需要大模型:比如“查一下运单号SF123456”,本质是意图识别加工具调用。我们后来在入口加了一个意图路由层,命中“查轨迹”“查运费”这类强意图直接走工具链,不经过完整RAG和记忆链路,响应时间从5秒压到2秒以内。这也是向agentic rag方向走的第一步,把一部分编排逻辑前置,而不是每次问答都走全链路。
7.3 保持简单是最难的
回顾这套系统,真正花时间的不是调大模型,而是把知识库拆好、把记忆写成可衰减的结构、把工具边界划清楚。Spring AI迭代速度很快,我写这套的时候接口还是1.x风格,内部已经有人在试用2.0的快照了,不少API变得更好用,但RAG、记忆、工具调用这三件套的职责划分和协作方式,整个思路是稳定成立的。如果你也在做类似的客服系统,我最后的建议是:先把三类问题分清楚,再动手写代码。否则你会在“AI什么都能干”的幻觉里,把一个本应该很清晰的项目,做成一个别人完全不敢接手的大泥潭。