ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Agent实战:给大模型装上工具层与知识层(RAG)

Java Agent实战:给大模型装上工具层与知识层(RAG) 做Agent做到第三个阶段很多朋友会卡在一个非常现实的问题上模型只会“说”不会“做”更不知道“查”。你让它帮你查订单物流状态它敢直接给你编一个“已发货”你问它公司内部的售后规则它拿通用互联网知识硬答一通。这套路放在demo里没问题放到生产环境里分分钟出事。这篇实战聊的就是AgentScope Java里的知识与工具层用一句大白话总结——一样一样给Agent装上手和书架。先解释一下这两个比喻。手对应的是工具层Tool / Function Calling解决的是“Agent能做什么”的问题查库存、下订单、调接口、改数据库这些都是模型原本不会的动作需要你把手接上去。书架对应的是知识层RAG / 知识库检索解决的是“Agent知道什么”的问题模型训练时没见过你的企业私有文档你需要给它配一个书架它提问的时候自己翻书而不是瞎编。这篇内容适合谁正在用Java做Agent落地的开发者尤其是已经跑通了“Agent能聊天”但卡在“Agent能干活”这个阶段的朋友。不管你是用AgentScope Java 2.x也好还是用其他Java Agent框架也罢工具层和知识层的设计思路是通用的代码层面做适当迁移就行。下面我按自己实际项目里的做法把一个带手和书架的Agent从零到一搭起来。1. 知识与工具层Agent的“手”与“书架”到底解决什么问题1.1 为什么必须在模型之外单独做这两层先把根本逻辑捋清楚。大语言模型本质上是一个“通才”它基于海量公开语料训练出来天生擅长语言理解、生成、推理但它有两个致命短板。第一个短板是没有实时业务能力。模型不知道你系统里这张订单现在是什么状态因为它训练时根本没见过你的数据库模型也不能帮你调用接口把订单取消掉因为它只是一个概率推理引擎本身没有执行能力。这个缺口就是工具层要补的。第二个短板是没有私有知识。你公司的售后政策、产品手册、内部流程、历史FAQ模型一概不知道。你说“我们支持七天无理由退货”模型会顺着话头说“好的没问题”但实际上你公司规定生鲜类目不支持退它就不知道。这个缺口就是知识层要补的。所以一个能真正干活的Agent必须同时具备三样东西大脑模型、手工具层、书架知识层。大脑负责理解和决策手负责执行动作书架负责提供决策依据。三者缺一不可这和我们人做事是同一个逻辑——你光会想不行你得会动手你想做对得先查对资料。1.2 工具层的设计逻辑动词与名词分离工具层本质上是把一组“动词”暴露给Agent。模型在对话过程中如果判断用户的需求需要调用某个动作来完成它就会在回复里带上一个结构化的调用意图由AgentScope Java的运行时去真正执行这段逻辑再把执行结果返回给模型继续推理。这里有个关键点经常被新手忽略工具定义的质量直接决定模型会不会用、用得准不准。模型不会“看”你的Java方法名它只看你给它的工具描述和参数说明。描述写得含糊模型就会在不该调用的时候乱调用参数说明写得不清楚模型就会传错参数。后面我会专门讲怎么把工具描述写“到位”。1.3 知识层的设计逻辑不是把书架背给模型听很多刚接触RAG的人会有一个误区既然模型不知道这些文档那把文档全塞进Prompt里不就行了这个做法在文档量小的时候勉强能用文档一多就彻底崩溃。一是成本问题几千甚至上万个Token的Prompt每轮对话都要重新传给模型费用和延迟都会成倍上涨。二是效果问题业界对“Lost in the Middle”现象已经有很充分的实验验证——模型在长上下文里对中间位置内容的关注度显著下降你塞得越多它越容易忽略真正关键的信息。所以知识层的正确做法是把文档离线切成小块做向量化建索引等用户提问的时候先把问题向量化去索引里检索出最相关的几个片段只把这几个片段拼进Prompt。这个过程就好比把整书架的书变成一张目录卡用户问“退货政策”时你只把对应那一页递给他而不是把整本书砸他脸上。这就是RAGRetrieval-Augmented Generation检索增强生成的基本框架。2. 工具层落地给Agent装上手2.1 工具定义的基本方式一个方法就是一个工具在AgentScope Java里工具层最基本的单元是“工具方法”。我习惯的写法是把一组相关的业务操作写成一个普通的Java类在方法上用注解声明这是可被Agent调用的工具然后用一个管理器统一注册。下面这段代码是我在项目里的一个简化示例场景是电商客服Agent的订单查询工具。public class OrderTool { ToolDesc(根据订单号查询订单的当前状态、支付时间、物流单号和物流公司) public String queryOrder(String orderId, ToolDesc(用户手机号后四位用于身份校验) String mobileSuffix) { // 业务逻辑调用订单服务查询订单状态 OrderInfo info orderService.queryByOrderId(orderId); if (info null) { return 未查询到订单号 orderId 对应的订单; } if (!info.getMobileSuffix().equals(mobileSuffix)) { return 手机号校验失败拒绝返回订单详情; } return String.format(订单状态%s支付时间%s物流单号%s物流公司%s, info.getStatus().getDesc(), info.getPayTime(), info.getTrackingNo(), info.getExpressCompany()); } ToolDesc(查询当前用户的售后申请进度参数为订单号) public String queryAfterSaleProgress(String orderId) { // 业务逻辑查询售后单状态 return afterSaleService.queryProgress(orderId); } }这里有几个细节我想多说两句。第一工具描述一定要写“什么时候用”。比如queryOrder的描述我写了“根据订单号查询订单的当前状态”但没有写清“当用户咨询物流时也可以用它”模型有时候就想不到。后来我改成“查询订单状态、物流、支付信息用户问物流/发货/到哪了都可以调用”召回率明显提升。第二所有可能引发歧义的参数都要加上ToolDesc模型不是人它看到mobileSuffix这个字段名不一定知道要传手机号后四位但看到说明就明白了。2.2 参数绑定与类型转换模型传参比你想的“粗糙”工具调用链路里最容易出问题的一环就是模型生成的参数和你的Java方法签名对不上。模型生成的是JSON结构AgentScope Java要做的是把这个JSON结构绑定到你方法的参数列表上。类型、必填项、格式每一项都是一道关卡。最常见的几个坑我列一下都是我实际踩过的。第一数字类型对不上。模型可能把1传成1把1.0传成1虽然逻辑上是同一个值但类型转换如果不做宽容处理就会报错。我处理的办法是参数绑定层不做强校验允许数值类型的字符串自动转成对应的数值类型。第二日期格式乱。用户说“上周的订单”模型可能在工具参数里生成“上周”而不是一个具体的日期字符串。这种情况不要指望模型能精确换算时间更好的做法是让工具方法内部做宽松解析。第三枚举值乱传。模型不知道你的OrderStatusEnum里只有PENDING、PAID、SHIPPED三个值它可能传一个COMPLETED进来。我一般会在工具方法内部做一次兜底映射遇到无法解析的值就返回明确的错误提示而不是让异常抛出——因为抛出异常对模型来说只是一个不透明的错误它还知道怎么改但如果你返回“ORDER_STATUS_UNKNOWN”模型就能读懂并尝试用其他方式询问用户。我建议你在项目里抽一个ToolParamParser来做统一解析把JSON字符串转成方法参数的过程集中管理方便加日志、加校验、加兜底。示例代码如下。public class ToolParamParser { private static final ObjectMapper MAPPER new ObjectMapper(); public static T T parse(String paramJson, ClassT clazz) { try { return MAPPER.readValue(paramJson, clazz); } catch (JsonProcessingException e) { throw new IllegalArgumentException(参数解析失败: e.getMessage()); } } }2.3 工具注册与会话内调度模型怎么知道该用哪个工具定义好之后需要注册到Agent的运行时里。注册方式通常有两种手动注册和自动扫描。手动注册简单直接适合工具数量少的场景自动扫描适合中大型项目让框架从Spring容器中自动收集所有标了AgentTool的Bean。ToolRegistry registry ToolRegistry.createDefault(); registry.register(new OrderTool()); registry.register(new UserTool()); registry.register(new ProductTool());工具多了以后会有一个新的问题模型每次决策都要看一遍它所有可用的工具。工具数量上到几十个以后不仅Token消耗变大模型选错工具的概率也会上升。我的建议是给工具做分组不要让一个Agent挂全量工具。比如客服Agent只挂订单、售后、商品知识这三组工具财务Agent只挂账单、发票两组工具减少模型的“选择噪声”。在运行时层面AgentScope Java会走ReAct循环模型先生成意图比如“调用queryOrder”运行时找到对应工具方法并执行把执行结果作为Observation返回给模型模型再基于结果生成最终回复。这个循环其实我们平时用肉眼是能追踪的后面我会讲怎么打开日志观察每一步。2.4 工具执行的安全底线超时、异常与幂等工具层上线容易做好安全加固难。这里没有太多花哨的技巧只有三条硬性原则我建议你写进团队规范里。第一所有工具都要有超时控制。外部接口有可能变慢、被限流、宕机这会导致Agent整个ReAct循环卡死。我一般会在工具方法内部统一设置HTTP调用的连接超时和读取超时比如连接超时3秒、读取超时8秒超过就直接返回“服务暂时繁忙请稍后再试”这样的结果而不是干等。第二工具异常不要直接抛出去要转成可读的错误信息返回给模型。比如查询订单失败返回“订单服务异常timeout”比抛出一个NullPointerException要友好得多。模型看到前者会自己考虑换一个工具或者向用户道歉看到后者只会不知所措。第三注意工具幂等性。查询类工具好说关键是那些会改数据的工具。一个“创建订单”的工具如果被模型重复调用两次用户就会收到两个重复订单。我的处理方式是在操作类工具里加一个幂等键参数比如把bizId作为参数传入业务侧根据bizId去重重复请求直接返回首次结果。3. 知识层落地给Agent配上书架3.1 知识层整体架构离线建库在线检索知识层和工具层有一个本质区别工具层是在线调用的知识层大部分工作是在线下完成的。你不能等用户提问了才开始切文档、算向量、建索引那样延迟会高到不可接受。合理的架构是“离线建库、在线检索”两部分。离线建库的流程固定为四步加载文档、切分文本、向量化、写入向量存储。这四步通常做成一个定时任务或运维脚本在项目启动时或者数据更新时执行一次。在线检索的流程也固定为四步接收用户问题、问题向量化、在向量存储中召回TopK候选、组装上下文返回给模型。AgentScope Java对这两块都有对应的组件抽象我这篇用一个通用的流程来演示你理解链路后切到具体框架实现都很容易。3.2 文档加载与切分策略尺寸太小精度差太大放不下文档加载相对简单市面上成熟的解析器都能处理PDF、Word、Markdown、TXT这些常见格式。PDF加载要注意表格和扫描件的问题纯文本解析对表格结构还原能力很差如果文档里有大量业务表格建议先把表格单独抽出来转成结构化数据或者用支持版面分析的解析器否则切出来的块会是一堆不可读的碎片文本。切分才是真正需要下功夫的地方。切分策略直接决定检索质量。我常用的是“递归字符切分法”核心逻辑是设定一个目标块大小先尝试按大分隔符切如果切出来的块还太大就递归用更细的分隔符继续切。这个思路能保证块之间尽量保持语义完整不会生硬地把一句话拦腰截断。切分参数上我的经验值是这样的。参数推荐值中文场景说明chunkSize400~800字符太小则单块信息量不足太大则向量语义被稀释chunkOverlap50~80字符让相邻块之间保留重叠内容避免关键信息被切断在边界分隔符优先级段落 换行 句号 逗号尽量按语义边界切先大段再句子这里有个真实案例。我们之前切一份售后规则文档用了500字符的块大小不设重叠。结果用户问“生鲜商品退货需要保留原包装吗”明明文档里有这条规则但这句话被切断在两块的边界上两块都只含一半信息语义向量都不够清晰检索就是召不回。后来改成600字符、重叠80字符这个问题就消失了。经验告诉我重叠不是浪费是在给边界信息上保险。3.3 向量化与向量存储选型维度对齐是入门第一课文档切好之后下一步就是把每一块文本转成向量。向量化的核心是选择合适的Embedding模型Java集成时通常会调用Embedding服务的HTTP接口或者SDK。选择时看几个指标向量维度、效果、成本、延迟。我个人的建议是如果是中文业务场景选中文效果好的Embedding模型维度在1024左右是常见值如果是中英混合场景要选支持多语言的模型如果对延迟敏感选轻量级模型或者给向量化接口加缓存。向量维度关系到后续向量存储的索引构建和检索性能同一个库里一定不能混用不同Embedding模型否则维度不一致检索直接报错这是新手最常踩的坑。向量存储的选型我按场景分三档。第一档是快速原型和测试环境直接用内存向量存储就够了。数据量小重启重建也没什么成本开发调试非常方便。第二档是小规模生产环境数据量在几十万条向量以内用一个单机向量数据库或者用了向量插件的搜索引擎就够用运维成本低。第三档是大型生产环境数据量到百万级、千万级就需要专门的向量数据库了支撑分布式部署、标量过滤、混合检索这些能力。3.4 检索策略TopK、阈值与重排向量化也好、存储也好最终目标都是为了把用户问题变成检索请求拿到最好的几个片段。检索策略上有几个参数要会调。第一个参数是召回数量TopK。我的经验是初检的时候多召回一些比如TopK取8到10免得漏掉关键内容然后再由后续步骤去粗取精。TopK取太少容易漏取太多后面组装Prompt时又会超Token上限所以一般取中间值。第二个参数是相似度阈值。向量存储返回的每条结果会带一个相似度分数低于阈值的直接丢掉。阈值怎么定用一堆标准问题去评测看召回结果的分数分布选一个能过滤掉噪声、又不误杀有用信息的临界点。不同向量模型算出来的分数范围差异很大不要照抄别人的阈值。第三个参数是重排。简单场景下直接把向量检索的结果按分数降序取前几块就够了复杂场景下我建议加一个重排步骤用LLM或者CrossEncoder对召回的十个块做一次相关度打分只取前三个。这个步骤会增加一些延迟但在答案准确率要求高的问答场景里非常值得。组装上下文的时候还有一个铁律控制总量。我一般把知识片段的总Token限制在800到1200之间加上工具定义和对话历史整个Prompt保持在模型上下文窗口的50%以内低于这个预算就做截断保证模型有充足的推理空间。4. 完整实战把“手”和“书架”装到同一个Agent上4.1 Maven依赖与项目结构我先给出项目基础依赖这里默认你已经有一个跑通的Java项目还没有的可以从Spring Boot 3.x脚手架起步。dependency groupIdcom.alibaba.agentscope/groupId artifactIdagentscope-java-core/artifactId version2.0.0/version /dependency dependency groupIdcom.alibaba.agentscope/groupId artifactIdagentscope-java-knowledge/artifactId version2.0.0/version /dependency依赖版本建议以你项目实际拉取到的为准AgentScope Java迭代节奏比较快用新不用旧。项目结构我习惯分成五块tool放工具类knowledge放知识库构建相关agent放Agent编排model放模型接入service放业务逻辑。src/main/java/com/example/agent/ ├── tool/ │ ├── OrderTool.java │ └── ProductTool.java ├── knowledge/ │ ├── KnowledgeBuilder.java │ └── KnowledgeSearcher.java ├── agent/ │ └── CustomerServiceAgent.java ├── model/ │ └── ModelProvider.java └── service/ └── OrderService.java4.2 定义业务工具以客服场景为例客服Agent对于演示工具层和知识层结合最合适工具负责查数据知识负责读规则。这里我把订单工具和商品工具放在一个CustomerToolkit里统一注册。Component public class CustomerToolkit { ToolDesc(查询订单实时状态用户询问物流、发货、签收时都可以用) public String queryOrder(String orderId, String mobileSuffix) { // 调用订单服务 } ToolDesc(查询商品当前售价、库存和规格参数为商品ID) public String queryProduct(String productId) { // 调用商品服务 } ToolDesc(查询售后申请进度参数为售后单号) public String queryAfterSale(String afterSaleNo) { // 调用售后单服务 } }注意看每个描述里都带上了“什么时候用”的信息。我遇到过很多同事写工具描述只写“查询订单”模型根本不知道怎么把用户的话和这个工具关联起来。工具描述其实是在给模型写“使用说明书”说明书越具体模型越不容易糊涂。4.3 构建知识库离线流程演示知识库我选用一个简化示例启动时从项目资源目录加载一份售后规则Markdown文档切分、向量化后写入内存向量存储。Configuration public class KnowledgeConfig { Bean public KnowledgeSearcher knowledgeSearcher(EmbeddingModel embeddingModel) { // 1. 加载文档 DocumentLoader loader new MarkdownDocumentLoader(classpath:docs/after-sale-rules.md); ListDocument docs loader.load(); // 2. 切分文本块大小600重叠80 TextSplitter splitter new RecursiveTextSplitter(600, 80); ListDocument chunks splitter.split(docs); // 3. 批量向量化 ListVectorData vectors embeddingModel.embedDocuments(chunks); // 4. 写入向量存储 VectorStore store new MemoryVectorStore(embeddingModel.getDimension()); store.addAll(vectors, chunks); return new KnowledgeSearcher(store); } }离线构建逻辑看起来短但每个环节都有值得注意的细节。文档加载阶段要确认文件编码中文文档乱码是常见问题切分阶段要在本地抽样看切出来的文本是否语义完整向量化阶段要做失败重试和日志记录批量接口一次调用几百条很容易被服务端限流我一般会分批次处理每批50条。在线检索时我把知识库检索封装成一个knowledge_retriever工具暴露出去让Agent将知识检索当成一次工具调用。这样设计非常灵活Agent回答规则类问题时会自动“翻书架”回答状态类问题时会自动“动手查”。Component public class KnowledgeRetrieverTool { private final KnowledgeSearcher searcher; public KnowledgeRetrieverTool(KnowledgeSearcher searcher) { this.searcher searcher; } ToolDesc(查询售后规则、退款政策、物流政策等公司内部知识当用户询问规则条款时调用) public String searchKnowledge(String query) { return searcher.searchWithContext(query, 3); } }4.4 组装Agent并验证效果所有部件齐了之后组装Agent这一步反而简单就是把模型、工具列表、知识检索工具和Agent核心组件拼起来。Service public class CustomerServiceAgent { PostConstruct public void init() { ChatModel model ModelProvider.getChatModel(); ToolRegistry tools ToolRegistry.createDefault(); tools.register(new CustomerToolkit()); tools.register(knowledgeRetrieverTool); ReActAgent agent new ReActAgent( model, tools, ReActConfig.builder() .maxIterations(5) .knowledge(knowledgeSearcher) .build() ); // 注册为Spring Bean供业务层使用 } public String chat(String userMessage) { return agent.chat(userMessage); } }验证效果我用两个标准场景测。第一个场景是状态类问题用户说“帮我看看订单20240712001到哪了”。Agent应该先走工具层调用queryOrder拿到物流信息再整理成自然语言回复。第二个场景是规则类问题用户说“生鲜订单能不能七天无理由退货”。Agent应该调用searchKnowledge去知识库里检索售后规则然后基于规则内容回答。如果Agent行为不对最有效的调试方式是把ReAct循环的日志打开看每一轮模型选择调用了哪个工具、传了什么参数、工具返回了什么结果。日志一打开“Agent为什么这么答”的原因就一目了然大多数问题都出在工具描述不清或者知识切分不合理上。5. 常见问题与排查技巧实录5.1 工具调用报错参数解析失败的排查现象Agent明明选中了正确的工具但调用时报IllegalArgumentException: 参数解析失败。排查思路先打开日志看模型生成的原始工具调用参数JSON看键名和你的方法参数名是否一致。AgentScope Java在做参数绑定时默认是按照参数名进行匹配的如果模型生成的是order_id而你的方法参数名是orderId匹配就会失败。解决的办法我总结几条一是加JsonProperty(order_id)这类注解做映射二是在工具描述里明确“参数orderId是订单号字符串”让模型生成尽量规范的字段名三是写一个宽容的ToolParamParser对key做大小写和下划线的多态兼容。我遇到过最恶心的场景是模型把两个参数合并成一个传这种只能靠增加描述精确度来规避。5.2 检索质量差召不到和召不准是两件事“召不到”指的是知识库里明明有这段规则但检索没把它返回。重点检查切分方式和向量化这两环。切分边界是否把关键信息截断块大小是不是太大导致向量语义被稀释Embedding模型的中文效果是不是太弱同时也检查一下用户的问法和文档原文在表述上差异是不是太大比如文档写“生鲜不支持七天无理由”用户问“水果坏了能退吗”语义跨度太大时向量检索效果会打折扣这时候可以考虑给知识库补几条同义表达或者用查询改写去扩展泛化问题。“召不准”指的是返回了相关片段但排序靠前的是不相关内容。优先调整TopK和相似度阈值增加重排步骤或者检查是不是分块里包含了大量无用噪声信息让向量相似度计算被干扰。处理办法是把文档里的页眉页脚、导语、模板文字这些噪声清理干净后再切分。下面这张表是我常用的排查速查表。现象可能原因解决动作工具调用报参数解析失败参数名不匹配检查模型生成JSON key名加映射注解工具被错误调用工具描述不够具体在描述中补充“什么时候用”知识检索召不回切分不合理/Embedding弱调整chunkSize与重叠换中文增强模型召回的片段不相关阈值太低/噪声干扰提高阈值清理噪声文本加重排答案生成过短Prompt上下文过长导致信息丢失控制知识片段总Token压缩上下文5.3 上下文过长知识块和工具结果把Prompt撑爆当Agent同时接了十几个工具定义、知识检索结果、历史对话以后Prompt很容易超长。上下文一长延迟变大费用变高模型还容易“注意力涣散”答非所问。我的处理经验有四个方向。工具定义做精简工具描述控制在50个中文字以内删掉废话给工具分组不要让模型每次看到全量工具。知识片段做压缩检索回来先截取关键段落再拼入Prompt。历史对话做滑动窗口只保留最近五六轮更早的内容做摘要。最后是设置整体预算Agent在组装Prompt时如果组合内容超过预算优先截断历史对话保留工具定义和知识内容。5.4 并发与性能消息量上来之后怎么办单体Agent跑测试没问题并发一上来就会暴露性能问题。知识检索和工具调用这两个环节是主要瓶颈。知识检索这边向量存储查询是高频操作一定要加缓存。同一类问题在短时间内重复出现很常见用户都问“退款多久到账”答案引用的知识块是一样的完全可以用缓存把重复检索挡掉。工具调用这边外部API的HTTP连接要用连接池复用不要每个请求都新建连接数据库查询要控流做一层本地限流防止Agent被用户刷爆后把下游订单系统拖垮。我见过最典型的线上事故就是Agent工具直接查数据库用户高并发访问时一次对话循环里查了好几次库数据库连接被打满整个服务挂了。后来在工具方法上加了限流和缓存同样的流量再进来数据库的QPS直接降了两个数量级。6. 一点个人体会这个系列写到第三篇我自己的项目经验也跟着走了一遍完整的闭环。先说结论工具层和知识层先做哪一个没有绝对标准但我个人建议先把知识层搭起来再做工具层。原因很简单知识层能让Agent先在“不犯错”的基础上把话答对工具层则是让Agent从“答对”走向“办成”顺序反过来的话Agent会很早变成一个“很能干活但总是干错”的角色。工具数量也是一样克制比激进更重要。我自己试过一口气给Agent挂三十个工具结果模型选择成本暴涨经常选错工具。后来砍到十个以内按场景拆分效果反而好了很多。手不在多够用就好书架不必塞满常读常新才重要。最后一个实用小技巧送给你上线之前把你产品里的用户问题整理成一份标准评测集几十条就够了跑一遍Agent把失败案例记录下来看是工具层的问题还是知识层的问题。这个评测集每次改动后都跑一遍你的Agent会越改越稳而不是越改越脆。这些年做Agent项目最大的体会不是模型选得有多好而是工具层和知识层这些“外围工程”打磨得有多细。这套思路放到AgentScope Java之外的任何Agent框架上都成立希望这篇实战能帮你少走一些我已经走过的弯路。
返回列表