Agent开发实战:用AgentScope Java打通工具调用与RAG知识库,为智能体装上“手”和“书架”

发布时间:2026/10/5 5:43:57
Agent开发实战:用AgentScope Java打通工具调用与RAG知识库,为智能体装上“手”和“书架” 做 Agent 开发这件事前两篇我们把 AgentScope Java 的会话闭环跑通了Agent 能接住话、能记住上下文。但放到真实业务里这个 Agent 基本还是半残状态用户问一句查一下华东区上个月的销售额大模型再聪明也算不出内部系统的数用户问咱们公司的请假制度是什么模型训练数据里也不可能存在你司的《员工手册》。所以这一篇专门把 Agent 的两块外挂能力讲透工具层和知识层。通俗点说就是给 Agent 装上手和书架——手负责干活书架负责查资料。这篇内容主要面向正在用 AgentScope Java 做落地项目、准备把 Agent 从 Demo 推向生产的同学也会把我在实际项目中踩过的坑和取舍逻辑一并交代清楚。1. 为什么光有大模型不够工具层和知识层的分工逻辑1.1 大模型的两块先天短板先想清楚一个本质问题大模型为什么需要手和书架因为这个东西的天生能力边界就两条。第一它没有实时数据。模型的知识是训练那一刻冻结的训练完之后它就与世界断联了。你问它巴黎今天天气怎么样它能告诉你巴黎的气候类型但它不知道此刻巴黎在下雨还是出太阳。业务系统里的订单数据、库存数据、用户画像数据它更是看不到。这个东西像一位特别博学但从不接电话的学者头脑里装着截止到某个时间点的知识但没有任何渠道获取新信息。第二它没有执行能力。就算模型知道查销售额应该调用 querySales 接口它也没办法真的发起一次 HTTP 请求就算它知道这个客户投诉应该升级处理它也改不了工单系统里的状态。模型本质上是一个纯文本进、纯文本出的推理引擎它擅长的是判断、推理、生成而不是对真实世界产生影响。所以有了两个补位层工具层让 Agent 具备调用外部系统的执行能力知识层让 Agent 具备按需加载私有文档的信息获取能力。这两块做的都是大模型自己做不到、但是业务落地又离不开的事。1.2 工具层和知识层怎么界定职责边界很多刚开始做 Agent 的同学会把这两层混在一起觉得不都是给 Agent 塞更多信息嘛。我建议用一个简单的标准去切分如果答案是算出来的走工具层如果答案是写好的走知识层。具体点说维度工具层知识层存储形态可执行的方法/函数/API文档切片与向量索引内容性质过程性知识知道怎么做陈述性知识知道是什么典型例子查数据库、发审批、调天气API员工手册、产品说明、行业报告实时性每次调用都是最新结果依赖文档更新频率失败表现超时、报错、返回异常码检索不到相关片段拿客服场景试一下用户问我的订单退到哪一步了这是实时状态必须调订单系统接口这就是工具用户问退货要满足什么条件这是客服政策里写死的条款应该去知识库检索这就是知识。判断清楚了后续架构才不会拧巴。1.3 AgentScope Java 里这两层落在哪在 AgentScope Java 的工程模型里工具和知识库都是以组件的形式挂载到 Agent 上的。Agent 构建时可以同时挂工具注册表和知识库服务运行时由 Agent 的调度逻辑来决定什么时候用哪一层。我之前在项目里是这样组织的简化示意ToolRegistry toolRegistry ToolRegistry.createDefault(); toolRegistry.register(new OrderQueryTool()); toolRegistry.register(new ReturnStatusTool()); KnowledgeBase kb KnowledgeBase.builder() .retriever(new PgVectorRetriever(datasource, embeddingModel)) .build(); Agent agent Agent.builder() .name(customer_service_agent) .model(chatModel) .tools(toolRegistry) .knowledge(kb) .build();这套设计的好处是工具和知识库对 Agent 的主逻辑是透明的。Agent 只知道自己有手和书架具体怎么注册、怎么更新由各组件自己维护。后面两节分别把这两块展开讲。2. 工具层把 Java 方法变成 Agent 能看懂的技能清单2.1 工具的本质一张写给 LLM 的点菜单工具层的核心问题不是怎么做工具而是怎么让模型知道有这个工具、并且会用这个工具。你想想 LLM 的工作方式它每次生成回答时看到的是一段文本里面写了系统提示词、用户问题、历史消息。如果你不把一个工具的信息用文本形式告诉它它根本不知道你代码里有个 querySales 方法。所以工具注册的本质是把 Java 方法翻译成一段 LLM 能读懂的点菜单——菜名是什么、有什么招牌菜、需要下什么单。业界主流的几种实现叫法不一OpenAI 那边叫 function callingClaude 那边叫 tool useAgentScope Java 里统一抽象成 Agent Tool。实际落地的核心结构都一样三个要素缺一不可name工具名。短小、语义清晰比如 querySales 就比 tool_001 好一万倍。description一句话说明用途。这是最关键的一段文本直接决定 LLM 在什么场景下选择这个工具。写法上要含触发条件我通常写成当用户需要查询指定区域、指定时间范围的销售额时使用返回金额及环比变化。parameters参数 JSON Schema。告诉 LLM 要填哪些参数、参数是什么类型、哪些必填、取值范围是什么。大部分工具调用失败根本不是代码问题而是 description 写得含糊、parameters 定义不严谨导致模型不知道什么时候该用、参数怎么传。给 LLM 的菜谱不够清楚它当然会乱点菜。我自己有个习惯写完工具描述后会拿几个真实用户问句去跑一遍看看模型能不能准确触发。如果 10 句里触发了 8 句以上这个描述就是合格的。触发率低就反复改 description不用怀疑模型笨先怀疑自己没写明白。2.2 注解驱动的工具 Schema 自动生成在 Java 工程里最省心的做法是用注解把工具方法的元数据写在代码边上让框架自动生成 Schema而不是手写一大坨 JSON。手写 Schema 的问题在于维护成本Java 方法改了一个参数Schema 没同步改模型就会按旧参数调用然后报错。这个不一致问题在工具多了以后非常痛苦。所以我强烈建议用注解声明让方法签名成为单一事实来源。我在 AgentScope Java 项目里的实现思路是这样参考实践中常见的写法AgentTool( name query_sales, description 查询指定区域、指定月份的销售额返回金额和环比。当用户问销售额、营收、业绩时使用。 ) public SalesResult querySales( ToolParam(name region, description 区域名称枚举华东、华南、华北、西南, required true) String region, ToolParam(name month, description 月份格式 yyyy-MM例如 2024-06, required true) String month) { return salesService.queryByRegionAndMonth(region, month); }框架在启动时扫描带有AgentTool注解的方法反射读取参数信息自动拼装出 JSON Schema。这样业务侧只维护 Java 代码一份Schema 永远跟随方法签名更新从根上消除了改了方法忘改描述这类低级事故。不过反射有一个小坑要注意注解里拿到的参数名不一定准确如果 IDE 没有开启-parameters编译参数反射拿到的会是 arg0、arg1。所以我在ToolParam里显式写 name不依赖反射的参数名能力。2.3 返回值结构化别让 Agent读天书工具调用的终点不是方法返回了而是模型读懂结果并能组织成对用户有用的回答。如果工具返回给模型一堆裸数据模型还要费劲去猜这串数字什么意思回答大概率会跑偏。我举一个对比的例子。假设工具返回的是{ code: 0, data: {total: 12345678.90, last: 8000000.00} }这个结构对 LLM 来说就很不友好12345678.90 是什么币种last 是上月的吗code 为 0 又代表什么模型只能靠猜猜错了就是胡说八道。同样的查询我建议返回完全自解释的结构public class SalesResult { private String region; // 华东 private String month; // 2024-06 private BigDecimal amount; // 12345678.90 private String unit; // 元 private BigDecimal monthOverMonthRate; // 环比 12.3% private String summary; // 一句话结论方便模型直接引用 }特别注意 summary 这个字段。我的做法是在工具内部就把结论性语句算好比如华东区 2024 年 6 月销售额为 1234.5 万元环比增长 12.3%。模型拿到这个字段可以直接组织成回答句子准确率会高很多因为关键计算已经在代码里完成了不依赖模型做数学运算。另外异常不要直接抛给框架。任何工具都可能失败应该捕获异常后返回一个带 error 标记的结构化对象比如{error: true, errorMsg: 查询超时请稍后重试, fallback: 当前无法获取销售数据}模型看到 error 标记之后能主动向用户解释系统暂时查不到而不是模型开始编数字。2.4 工具注册、动态裁剪与执行容错工具多了以后会遇到一个新的问题模型选择太多容易选错。工具挂在 Agent 上不代表每一次对话都应该把所有工具描述都塞进上下文。实测下来把几十个工具的 description 全部放进 Prompt模型的选择准确率会明显下降而且 token 消耗也大。我采用的思路是动态裁剪在每次对话进入模型之前先根据用户问题做一轮相关性排序只保留最相关的若干个工具描述在 Prompt 里其余暂时隐藏。相当于书架上的工具随手就能拿到但先根据问题把最可能用的几样摆在桌面上。相关性排序最简单的是做文本相似度拿用户问题去和每个工具的 name description 做向量匹配取 topK。如果项目里还没上向量库直接用关键词匹配也能凑合但效果差不少。我建议既然已经要做知识层了Embedding 模型顺手就能用来做工具裁剪一份投入两处收益。执行容错也很重要。工具调用链路可能挂在任意环节——模型生成参数格式不对、工具执行超时、下游系统报错。框架层面我做了这样几件事参数解析失败时把错误信息回传给模型让它重新整理参数再试一次最多重试两次工具执行设置独立超时避免某个慢接口把整个会话拖死工具抛出的原始异常一律打日志但回传给模型的是结构化错误消息不让模型看到 Java 堆栈。一条可参考的执行配置ToolExecutionConfig config ToolExecutionConfig.builder() .timeout(Duration.ofSeconds(5)) .retryTimes(2) .maxToolCallsPerTurn(5) .build();maxToolCallsPerTurn 也值得提一下它限制单轮对话内模型最多调用几次工具防止 Agent 在高复杂任务中陷入死循环无限调用工具白白烧钱。3. 知识层分块、向量化、检索——书架不是摆着好看的3.1 书架的组成从文档到可检索的切片知识层解决的是另一个问题让 Agent 能在需要的时候读到私有文档。先别急着想向量和相似度这些词先想一下我们平时怎么用知识库公司有几万字的《员工手册》你不可能把它整体糊进 Prompt——模型上下文放不下就算放得下也是一团丸子进汤捞不出重点。所以知识层的第一步是把文档拆成可检索的单元。这个过程的专业说法叫文档切片chunking我更喜欢比喻成把一整本厚书拆成一页页编好号的卡片放进索引柜里。流程大致是解析把 PDF、Word、Markdown、HTML 统一抽取成纯文本清洗去掉页眉页脚、表格错乱、重复段落、无意义字符切分把长文本拆成固定大小或按语义划分的块向量化每一块文本调用 Embedding 模型产出向量入库把文本片段 向量 元数据(文档名/章节/更新时间)写入向量库。这一环节的决定性工作在前三步。很多人以为向量库选型决定检索质量其实切分策略的影响往往更大切不好后面怎么调都没用。3.2 分块策略怎么选固定大小、标题感知还是语义切分分块这事没有银弹但有几条规律可循。固定大小分块是最省事的方案设定每块 300~500 token块与块之间重叠 50 token。优点是实现简单、性能稳定缺点是容易把一个完整语义切开导致检索结果半截话。比如一段制度条文刚好被切在员工请假 3 天以内需... 后面检索只拿到前半句模型回答就容易断章取义。标题感知分块是实际项目里最常用的方案先按文档的标题层级markdown 的 #、PDF 的章节结构切出大段如果大段内文本还超过设定长度再在段内二次切分。这样能最大程度保留语义完整性。我目前大部分知识库用的就是标题优先 定长兜底 块间重叠的策略先按标题切如果某个章节还是太长比如超过 800 token就继续切重叠 50 token 防止上下文断裂。语义切分更高级一些通过 Embedding 相似度去判断句子边界、动态聚合。这套方案效果好但计算成本高尤其文档量大时离线切片也会变慢。我一般只在制度条文、合同这类语义密集的文档场景使用普通场景用标题感知就够。给你一张选型参考表策略优点缺点适用场景固定大小简单、快、稳定语义断裂风险高纯说明文档、日志类文本标题感知语义完整、工程实现简单依赖文档结构质量手册、制度、产品文档语义切分边界自然、召回质量高成本高、耗时大合同、论文、问答对有一点要专门提醒网页爬下来的内容一定要清洗干净再入库下一篇相关推荐广告位这些噪声如果进了向量库检索时很容易以高相似度被捞出来把模型的回答带偏。我吃过这个亏后来在清洗环节加了链接文本过滤和页脚去重效果立竿见影。3.3 Embedding 模型与向量库选型Java 侧做知识层Embedding 模型通常不是直接内嵌在 Java 进程里的而是走 HTTP 服务调用。我试过的方案里BGE-M3 是个很稳的选择中英文效果都不错而且支持 1024 维输出兼容性好如果项目部署环境简单、追求低延迟也可以考虑 OpenAI 的 text-embedding-3-small 这类商业接口按调用量付费省去自建模型的运维成本。向量存储这层以 Java 团队最熟悉的资源来衡量我会优先推荐 pgvector而不是一上来就上专门的向量数据库。原因很直接大多数业务应用已经有 PostgreSQL 了pgvector 作为一个扩展装进去向量数据和应用数据在同一个事务里备份、权限、运维都省事小规模到中规模几十万到几百万向量完全够用。如果向量规模到了千万级以上或者对检索延迟极其敏感再去考虑 Milvus 这种独立向量数据库。选型不要过度设计先用最简单的架构跑通再按瓶颈升级。方案部署成本规模上限适合团队pgvector低复用现有 PG百万级Java 后端团队、早期验证Milvus高独立集群千万级以上大规模 RAG、高并发场景商业向量服务中按量计费弹性不想运维、快速上线向量化的调用在 Java 侧也很简单本质就是一个 HTTP POST传文本、拿向量。我封装了一个 EmbeddingClient 组件内部做连接池和缓存相同文本不重复调用顺便把模型服务的超时和重试策略统一管起来。3.4 检索结果如何安全注入上下文RAG 的标准姿势知识库建好了检索这一步才是决定回答质量的关键。完整链路是这样的用户问题 → 向量化 → 向量相似度检索 TopK → 重排序可选 → 注入 Prompt → 模型生成回答。很多人只做到向量检索 top5 直接丢进 Prompt效果差是有原因的。向量检索的 TopK 结果里经常出现语义相近但只是相关并不是有用的片段尤其当文档中有大量重复内容时。所以我会在向量检索之后加一个重排序环节用一个 rerank 模型对 TopK 做二次打分把真正切题的内容提到最前面。实测下来加了重排这一步回答的命中率能提升非常多代价不过是一个额外的模型调用。注入 Prompt 时我会在检索内容两侧包上明确的指令告诉模型这些是参考资料而非事实本身以下内容是从公司知识库检索到的相关资料可能不完整或已过时请结合资料回答问题。 若资料中没有相关信息请直接说明未找到不要编造。 引用资料时请注明资料编号。 资料1编号D-0231来源《员工手册》第三章 ...这个约束壳不是花架子。它主要解决两个问题一是抑制模型幻觉让它没资料时敢说不知道二是提供引用溯源后续可以把编号映射回原文这对企业场景来说几乎是必需的。还要注意检索时机。不是每一轮对话都需要检索只有当问题涉及私有知识时才值得检索。我的做法是在 Agent 的调度逻辑里加一个是否需要检索的判断命中知识类问题才调检索否则直接让模型回答。这一步既能省时间又能避免检索干扰模型对开放问题的回答。4. 手和书架的分工协作一次业务请求的完整链路4.1 请求如何走到工具层Agent 的决策路径工具和知识都挂上之后Agent 是怎么决定这个请求该调用工具的主流 Agent 框架普遍采用 ReAct 循环推理Reason→ 行动Act→ 观察Observe。AgentScope Java 内部也是类似机制。简化来看每次用户请求进来Agent 会先把可能的工具列表 检索到的知识片段 对话历史组装成 Prompt 发给模型模型输出的结果可能是普通文本也可能是一个结构化的行动计划——比如{ action: call_tool, tool_name: query_sales, arguments: { region: 华东, month: 2024-06 } }框架解析出这个 action去 ToolRegistry 里找到对应方法执行拿到结构化的结果把它作为观察结果填回上下文再让模型基于这个结果做下一步决策或直接生成最终回答。这个循环可以多轮进行直到模型认为信息足够、输出最终答复为止。这里有个工程细节模型可能输出不合法的 JSON所以框架层的 action 解析必须有容错。我的做法是让模型输出工具名 参数两段式文本再配一个宽松的正则/JSON 双通道解析器一次解析失败还能二次兜底。在这个环节卡住的概率比你想象的高。4.2 工具结果反哺知识检索两条线怎么汇合工具和知识不是各干各的很多复杂问题需要两层配合才能回答。最典型的模式是工具先查状态然后拿状态作为关键词去检索知识。举个例子。客服 Agent 收到用户问题我买的取暖器坏了想退货怎么操作这背后实际上有两件事先查订单系统确认这个用户的订单是不是在退货有效期工具层再查退货政策文档找到对应品类的退货规则知识层。顺序很重要——如果先查知识库拿到的是通用退货政策没有结合这个用户的具体订单状态如果只查工具那得到的只有一行状态数据没法解释退货政策。两条结果合并后模型才能给出您的订单已超过退货期但根据规定可以申请保修保修流程是……这类完整答复。在这个链路里工具返回的订单号商品 ID工单状态就是下一条知识检索的输入。我在实现上是把工具返回的结构化字段作为新的查询关键词去调检索器而不是继续拿用户原问题检索。这样检索精度高得多。工具和知识库就像两条河道在这里汇合才能推动模型这艘船往前走。4.3 多轮会话里的协同时序与缓存策略进入多轮对话之后协同就变得更复杂了。用户可能先问华东区销售额怎么样Agent 调工具得到一组数据接着用户追问为什么下滑了这时候 Agent 需要基于上一轮拿到的结果去知识库检索销售额下滑的可能原因或公司的分析报告。如果每一轮都重新调工具、重新检索成本太高。我的做法是在会话上下文里做结果缓存用户问题带上轮次的工具调用结果如果相同工具、相同参数在最近几轮已经被调用过直接复用缓存结果知识检索结果按查询关键词 文档范围做短时缓存用户在多轮里追问同一主题时能直接跳过重复检索缓存之外还要做失效判断如果用户说重新查一下必须强制刷新不能死守着旧缓存坑人。伪代码示意一下CachedResult cache context.get(CachedResult.class); if (cache ! null cache.matches(region, month)) { return cache.getValue(); } SalesResult result salesService.queryByRegionAndMonth(region, month); context.put(new CachedResult(region, month, result)); return result;另一个要注意的是知识检索的短期缓存在多轮里很有用但工具的缓存要非常谨慎尤其涉及最新状态的实时查询尽量不缓存需求反着来——该实时就实时该收敛就收敛。5. 生产环境下我踩过的几个坑超时、幂等、权限与索引冷启动5.1 工具调用的超时、限流与降级工具层上线之后第一个事故往往是超时。Agent 背后的大模型推理本身要好几秒生成完调用决策之后工具执行是另一个耗时段。这两个超时要分开设置。我的经验值模型推理超时设 30 秒工具执行超时设 5 秒。为什么工具要这么短因为模型在你回答用户之前已经等了很久了工具再跑 20 秒用户会直接关掉页面。工具超时后返回工具暂时不可用请稍后重试模型会把这个降级信息组织成回复而不是一直傻等。限流是另一个不能忽视的。Agent 一旦循环调用工具比如自动排查问题一轮里连续调同一个接口对下游系统的压力相当可观。而且测试环境里你可能感觉不到一上生产多个用户同时开多个 Agent 会话对应系统直接被打挂的情况我见过不止一次。所以每个工具都必须配独立的限流策略调用方也要有熔断机制。工具内部再快也架不住 Agent 并发地高频调用。5.2 幂等设计LLM 会重复调用同一个工具这个坑非常容易踩而且踩的人往往是在生产环境出了事故才回头补的。大模型生成的调用序列并不总是干净利落的我遇到过的情况包括模型在一次响应里连续输出两个相同的 tool_call网络重试导致同一请求被提交两次Agent 的自动重试机制和下游系统的重试叠加。如果工具是无副作用的查询那无所谓但如果是发送邮件创建退货单标记工单重复执行一次就是事故。所以凡是有副作用的工具必须做幂等设计。最简单的方案是框架层给每次工具调用生成唯一的 requestId透传给工具内部服务端用 requestId 去重同一轮会话里相同参数的工具请求如果已经执行过直接返回上一次的结果。把幂等等同于加个分布式锁是不完整的请求层面的去重才是关键因为 LLM 的重复调用往往发生在同一个会话上下文里。5.3 知识库权限别让 Agent 把机密文档检索出来知识层的检索结果会直接进入 Prompt这意味着如果检索不过滤权限机密文档就会通过间接方式泄露给提问者这比直接拿文件还隐蔽。比如一个普通员工问年度绩效奖金怎么分配的如果检索把高管激励方案这种机密文档捞了出来模型很可能直接引用其中的内容回答这就出大问题了。我的建议是向量切片入库的时候必须同时把访问控制信息写成元数据。检索阶段根据当前用户的身份和角色过滤掉无权访问的切片再做相似度查询。不要把希望寄托在Prompt 告诉模型不要泄露机密实测下来这种约束非常不可靠模型很可能在组合回答时不自觉地引用过滤不干净的切片内容。权限过滤的维度根据需要设置过滤维度示例实现方式组织范围部门、团队、子公司元数据中记录部门ID检索时强制匹配密级公开、内部、机密元数据中记录密级检索时按角色透传最大可见密级文档类型政策、制度、合同按业务场景限定文档集时间范围仅允许查看生效版本元数据中记录生效时间检索时加时间条件这个过程必须在检索阶段完成不能在生成阶段才做。检索前过滤是不可逆的——一旦切片被送进 Prompt再想收回就晚了。5.4 冷启动与索引更新别等上线前才想起来建索引知识库冷启动问题也很容易成为上线那天的拦路虎。假设客服知识库有几百份文档按标题感知切完可能有上万个切片逐个向量化即使是并发调用也要不少时间。要是没有提前构建索引上线当天才发现用户问什么什么都召回不到那种手忙脚乱我经历过。我的做法是文档导入和切片向量化做成离线的异步任务通过消息队列驱动。文档上传后进入队列worker 消费任务做清洗、切分、向量化、入库。这样文档量大就不怕后台慢慢消费前端不阻塞。同时维护一个增量更新状态表记录每个文档的最后处理时间文档改了就触发重新处理避免重复向量化旧内容。Embedding 模型的加载和部署也要提前想清楚。模型首次加载到内存需要时间和显存如果每次重启 JVM 都要热加载一次成本很高。方案是把 Embedding 模型做成独立的服务用 FastAPI 或者同类框架单独部署Java 侧只做网络调用。这样模型的生命周期和业务服务解耦升级模型、扩容都互不影响。最后提醒一点向量库里的旧切片不是删掉就完了文档更新后需要把旧切片标记为失效同时做重算入库。否则同一篇文档新旧版本混在一起检索回来的一半是过期内容模型引用旧政策回答比不回答还严重。这个版本管理 失效标记的设计一定不能省。把工具层和知识层都串起来之后我最大的体会是Agent 项目里最费劲的往往不是模型 Prompt 调优而是工具边界和知识边界的梳理。工具不是越多越好几十个工具模型选择准确率会明显下降知识库也不是越全越好切分和权限做不好喂给 Agent 的内容就是噪声和漏洞。我的建议是先小后大先把 5 到 10 个高频工具、500 篇以内的高质量文档跑通走完一遍完整的搜索、调用、生成链路之后再逐步扩展。这个顺序能让你在早期避开大部分坑后面再加量也只是个平滑扩容的事。