ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AgentScope Java实战:为Agent装上手和书架,打通工具与知识层

AgentScope Java实战:为Agent装上手和书架,打通工具与知识层 接手 Agent 项目之后我发现大家早晚都会卡在同一个问题上模型再强也只能想不能做。这次 AgentScope Java 实战第三篇我来讲讲知识与工具层——说白了就是给 Agent 装上手和书架。工具层负责让 Agent 能调用外部 API、查数据库、执行真实动作知识层负责把私有文档变成它能随时检索的内容。这两层配上模型能力一个真正能落地的 Agent 才算立起来。这篇文章我会用 AgentScope Java 的实际代码把工具注册、参数 schema、函数调用链路、知识库接入这些核心细节一层层拆开讲清楚每一步为什么这么设计以及我在线上环境里踩过的坑。适合已经用上 AgentScope、打算让 Agent 干点实事的 Java 开发者也适合那些正准备从Demo 能跑通走向生产能稳定的团队参考。1. 先搞清楚知识与工具层到底解决什么问题1.1 没有工具和知识的 Agent能力是断的一个纯靠模型参数的 Agent本质上就是一堆权重压出来的概率分布。你说帮我查一下订单号 20240312 的物流状态它可能会编一个看起来很像样的答案给你——这就是行业里常说的幻觉。不是模型不想诚实而是它脑子里根本没有这份物流数据也没有任何渠道去获取。解决方向就是这篇文章的主角知识与工具层。知识层解决它不知道怎么答的问题把私有数据通过检索塞进它的上下文工具层解决它知道但做不了的问题让模型把用户意图翻译成一次真实的 API 调用。两者合起来Agent 才有闭环执行的能力。这句话听起来简单落地时牵扯到的细节非常多——工具参数的 schema 怎么写、知识分块多大合适、模型能不能正确理解工具返回结果每一处都是坑。我做过的几个 Agent 项目里凡是卡在只能聊天、不能干活状态的几乎都是因为没把这两层做好。模型本身的能力反而是最不需要担心的部分现在的开源和商业模型指令遵循能力已经非常强真正的差距往往在它够不着的外部信息和外部动作上。1.2 知识、工具、记忆三个层次别混在一起刚开始做 Agent 的时候我特别容易把三样东西搞混知识库、工具、记忆。它们的边界其实很清楚我后来给团队培训时一直用这三句话来区分记忆Memory对话里和当前用户相关的动态信息比如刚才聊到我住在杭州后面回答要记得把杭州作为上下文。知识Knowledge外部静态资料库比如产品手册、工单历史、政策文档通过检索动态注入不是靠模型背下来的。工具Tool能执行的动作比如查物流、下单、发邮件、执行 SQL这是 Agent 对现实世界产生影响的通道。在 AgentScope Java 的设计里这三块是独立的模块官方也不建议把它们揉在一起。我见过有人图省事把聊天记录全量灌进向量库当知识用结果检索出来全是无关对话回答质量差到没法看也见过有人把调用某 API 拿数据和某 API 的说明文档混在同一个工具里模型根本分不清该执行还是该阅读。注意判断一个需求该放哪一层就一个问题——它是要读的资料还是要做的事。要读的进知识层要做的进工具层跟当前用户上下文相关的进记忆。分清楚这三层后面所有的实现和排错都会顺很多。2. 工具层实战给 Agent 装上手2.1 从一段最简单的工具注册开始AgentScope Java 里注册一个工具非常朴素本质就是给一个普通 Java 方法打上注解声明它是可被模型调用的工具。下面是我项目里常用的写法Tool( name query_express, description 根据订单号查询物流轨迹返回每一步的节点时间、地点和状态, params { Param( name orderId, type String.class, description 订单号长度 8-20 位的字符串例如 20240312001, required true ) } ) public String queryExpress(String orderId) { ExpressInfo info expressClient.query(orderId); return JSON.toJSONString(info); }写完后把它注册进 AgentToolManager toolManager new ToolManager(); toolManager.register(new ExpressTools()); Agent agent Agent.builder() .name(express_agent) .model(new DashScopeModel(qwen-max)) .tools(toolManager) .build();这里有个设计细节值得多说一句工具说明description不是给人看的注释是给模型看的使用说明书。模型会不会在合适的时机调用这个工具很大程度上取决于你这段描述写得够不够清楚。很多新手把 description 写成查物流模型在订单场景下可能知道要调用它但到了我的快递到哪了这种口语化表达时它就犹豫了。写成根据订单号查询物流轨迹返回每一步的节点时间、地点和状态模型就能更准确地建立意图到工具的映射。注册之后你不需要手写循环去处理模型要求调用工具 - 执行 - 把结果回传给模型这个闭环AgentScope Java 的消息循环会自动帮你做完。你只要保证两点工具方法有清晰的入参说明返回值可以被序列化成模型能读的文本。2.2 schema 设计直接影响模型能不能正确调用这是我在工具层踩得最狠的坑专门拿出来讲。模型调用工具不是自己猜参数它是根据你提供的参数描述也就是 schema来生成一个 JSON 结构。AgentScope Java 会根据注解自动生成这个 schema但字段写得不清楚模型就会填错。举一个真实案例我之前写过两个工具一个叫send_coupon发优惠券一个叫refund_order退款两个方法的入参都叫id。结果线上线下都翻过车——模型把给用户退款当成了给用户发券因为两个工具的第一个字段都叫 id描述写得也都含糊。后来我把参数名改成orderId和couponBatchId再把描述写细问题立刻消失错误率几乎降到了零。参数设计这块我给几条实际经验参数名用领域里最自然的命名别用无意义的arg1、data模型看到有意义的参数名理解成本会低很多。enum 类型的参数一定要把可选值列出来比如status只接受PENDING、SHIPPED、DELIVERED模型看到枚举值就基本不会乱填。可选参数不要设一个含糊的默认值让模型猜要把不传会怎样写清楚比如不传城市则默认定位到杭州。工具数量尽量控制在十几个以内。工具一多模型的选择准确率会肉眼可见地下降。如果确实工具很多就先做一个路由工具让模型先选业务分类再进入二级工具集。我还整理了一个小表格方便你对号入座问题典型原因推荐做法模型不调用工具description 太泛意图匹配不上把工具能力和适用场景写具体模型调用了错误的工具多个工具参数/描述相似区分参数名加强描述差异化参数值填错参数描述缺边界如单位、格式在 description 里写明格式和取值范围模型看不懂工具返回结果返回值嵌套太深或过长精简返回值结构突出关键字段2.3 工具执行链路与错误处理工具执行不是调完就完了它在 AgentScope Java 里是一条完整的链路模型输出工具调用请求 - 框架校验参数 - 反射调用你的方法 - 结果包装成消息 - 回传模型 - 模型生成最终回复。这条链路里最容易出问题的两个环节一个是参数校验一个是异常处理。参数校验如果失败不要直接抛 RuntimeException最好返回一个结构化的错误消息。Agent 的特点是错了可以再来一次模型看到错误原因后有能力修正参数重新调用。你如果直接抛异常框架把异常当最终结果返回整个对话就断了。我通常的做法是if (orderId null || orderId.length() 8) { return JSON.toJSONString(new ToolError(ILLEGAL_ARGUMENT, 订单号格式不正确请检查后重试)); }这样模型收到这段文字后会意识到是自己传参出了问题然后基于新的理解重新生成调用。这个机制是人机协同里特别值钱的一环——等于给了模型一次自我纠正的机会。另外一个经验是工具方法的返回值尽量精简。模型上下文窗口是有限的你把一个 50KB 的 JSON 直接返回给它有效信息反而被稀释了。我通常会在工具内部先做一次裁剪只返回关键字段。比如查物流只需要返回最新节点状态 时间 预计送达完整轨迹可以放在附件里或者让模型需要时再查一次别一股脑全塞进去。3. 知识层实战给 Agent 配书架3.1 什么时候该上知识库什么时候直接塞 prompt很多人一听到知识库就想到向量数据库但我的经验是知识库不是万能的也不是所有知识都得走检索。如果知识总量很小比如就几十条常见问题或者每条知识都很短比如 API 版本号、客服电话直接写死在系统提示词里反而更靠谱。检索是先召回再拼接本身有召回失败的风险为一个几十条的小清单引入一整套向量检索链路属于过度设计延迟和成本都划不来。真正需要知识库的是这几类场景文档量大超过了模型上下文窗口必须检索相关片段再拼进去。知识更新频繁比如政策条款、商品信息不能让模型把旧知识背死。需要限制回答边界比如客服 Agent 只能基于公司提供的资料回答防止模型自由发挥。AgentScope Java 对知识层的封装思路是知识库作为一个组件接到 Agent 上问答时框架会根据当前问题做检索把命中的文档片段作为额外上下文拼到 prompt 里。下面这个例子就是一个最小可用的知识库配置。3.2 用向量检索搭一个最小可用的知识层KnowledgeBase kb KnowledgeBase.builder() .embedder(new DashScopeEmbedding(text-embedding-v3)) .store(new MemoryVectorStore()) .chunkSize(512) .chunkOverlap(64) .build(); // 加载文档 kb.loadFromDirectory(data/product_manual);配置就这几行背后流程却不少文档被切分成固定大小的块chunk每块文本用嵌入模型转成向量存进向量存储用户提问时问题同样转成向量和库里所有向量做相似度计算取 top-k 个最相似的文档块作为上下文交给模型。这里最值得研究的是chunkSize和chunkOverlap这两个参数。chunk 太大单条文档块里混入太多无关内容检索精度下降而且拼进 prompt 时上下文消耗很快chunk 太小一个完整的语义单元被切碎模型看到的内容不完整。512 个字符中文场景大约二百多个字一般是个不错的起点chunkOverlap设 64 可以让相邻块之间有一点重叠避免语义被切断在边界上。embedder 的选择同样关键。原则上文档切块时用的嵌入模型和检索时编码问题的模型要保持一致否则向量空间的度量标准都不一样。如果你本地自建模型要注意中文场景别用一个只为英文训练的模型匹配效果会非常差。AgentScope Java 直接支持 DashScope 和 OpenAI 系列的嵌入接口做中文业务建议优先选对中文支持好的模型。3.3 召回质量调优分块、重排、混合检索知识库搭完只是开始真正考验的是召回质量。我做过一个售后知识库刚上线时命中率惨不忍睹用户问退款多久到账返回的全是退货地址相关的文档块。后来排查发现三个问题。第一分块策略太机械纯按字符切把退款流程说明和退货地址说明切进了同一个块检索时关联度被拉平了。后来我改成按 Markdown 标题和段落边界切块语义完整度高了很多。第二只用向量检索一个召回通道太依赖语义相似度遇到术语差异容易漏。像退款和退货退款词面差很远但语义很近向量模型能处理一部分但还不够稳。加一个 BM25 关键词检索通道和向量检索做融合RRF 融合算法效果会明显提升。第三召回量给得太少。top-k 我一开始设了 3后来发现相关文档偶尔排在 5 名开外干脆改成召回 8 个块再做一层重排rerank把真正和问题相关的块排到前面prompt 里只放前 3 个。这里多一次重排的算力开销很值上下文质量和 token 成本同时得到了优化。注意上线前务必拿 50 条真实用户问题做一遍召回测试而不是用两三条 demo 问题试一下就宣布完成。Agent 生产环境和演示环境的差距绝大部分都出在这种你以为没问题的地方。4. AgentScope Java 里的工具与模型适配细节4.1 不同模型的 function calling 能力差异大工具层能不能用得好一半看框架一半看模型。不同模型的 function calling 能力差异非常大。有些模型的工具调用格式支持不完整或者一次只能处理一个工具调用有些模型对参数类型非常挑剔int 类型传成了字符串它就拒绝执行。在 AgentScope Java 里底层模型可以通过配置动态切换这意味着你同一个 Agent 可以在不同模型之间换来换去。但我建议你在切完模型之后把工具的调用链路完整回归一遍重点看三个点工具调用是否支持多轮模型先调一个工具根据结果再调另一个、并行工具调用的数量上限是多少、返回的 JSON 字段顺序是否会影响模型解析。我在项目里遇到过最典型的现象是同一个工具 schemaA 模型能稳定调用B 模型大概 30% 的情况会跳过工具直接编答案。这不是框架问题是模型本身的指令遵循能力问题。解决办法有两个方向一是在系统提示词里加一句当需要获取实时信息时必须先调用工具禁止凭记忆作答二是把工具描述写得更结构化让能力弱一点的模型也能理解。这里要特别提一句别迷信某一个模型在 benchmark 上的工具调用分数真实业务里的字段描述、多步调用、异常恢复都会让分数大打折扣。最好的方式就是拿自己的真实工具跑一遍回归用例用事实说话。4.2 并发调用与性能瓶颈分析知识层和工具层一旦启用Agent 的响应就不再是一次模型调用那么简单了。一次问答背后可能有一次 embedding 查询、一次向量检索、多次模型推理、一次或多次外部 API 调用。整个链路的性能瓶颈往往不在模型本身而在这些旁路操作上。举个实测数据一个带知识库和两个工具的单轮问答纯模型生成可能只要 2 秒但加上向量检索 300ms、工具查询 500ms、以及模型调用工具-看结果-再生成的第二轮推理总时长会拉到 5 秒以上。如果有并发用户进来连接池耗尽、外部服务限流都会成为新的瓶颈。优化思路我整理了几条都是自己实测有效的embedding 结果做本地缓存同一个问题短期重复出现就不用重新编码。向量检索用批量查询接口避免串行多次检索。外部 API 调用统一走异步编排框架比如用 Java 虚拟线程或 WebClient别让一个慢接口拖垮整条链路。对 Agent 的决策-执行-再决策循环设置最大轮数上限防止工具调用陷入死循环——我在线上真的遇到过模型反复调用同一个失败工具卡了十几轮才超时。另外工具层最好单独做超时和熔断。外部接口不可能永远稳定工具调用一旦超时要有默认的降级方案比如返回服务暂时不可用而不是让整个 Agent 卡死。这一条在线上比任何调优都重要。5. 实操中踩过的坑常见问题与排查技巧实录5.1 工具参数序列化异常怎么排查工具调用最常见的报错就是参数转换失败。模型返回的是一个 JSON 字符串框架要把它映射成 Java 方法的参数类型一旦类型对不上比如模型把orderId: 123456传成了数字而你方法签名是 String框架可能直接抛异常。排查这种问题第一步是看框架日志里模型返回的原始消息体确认模型到底生成了什么。我见过太多人直接盯着异常栈看绕了一大圈。第二步是给参数 schema 加上更严格的格式约束在 description 里写明orderId 必须是字符串类型不要把数字传给接口。第三步如果模型经常传错可以在工具方法里做一层防御性转换比如用String.valueOf()兼容数字类型。这里还要注意一个 Agent 特有的问题模型会虚构返回值。工具方法明明返回了结果模型在生成最终回答时可能不按这个结果来而是凭自己的常识补充了额外细节。这种情况要在 prompt 里约束最终回答只能基于工具返回的数据禁止添加不存在的信息否则线上很容易出现工具查出来的状态是已发货Agent 张口就说已签收的荒谬结果。5.2 知识库命中率上不去的三个根源我梳理了做知识库最常遇到的三个问题基本覆盖了 80% 的召回不佳场景。第一文档没清洗直接入库。PDF 里最常见的问题是表格被拆散、多栏文字读取后顺序错乱嵌入模型对这些乱序文本基本无能为力。入库前一定要做文本清洗能转成 Markdown 结构就先转结构。第二问题与文档的语言风格差异过大。用户问的是大白话钱什么时候退我文档里写的是退款时效说明向量检索往往匹配不上。解决方案是准备一批问法映射文档或者把常见问题的问答对做成独立的小块入库。第三top-k 与重排没有联调。只调大 top-k 会引入噪声只调重排模型不调召回通道解决不了召回源头的问题。正确顺序是先看召回通道能不能把你手工标注的相关文档捞进来再优化重排把它们的排序提前。最后放一个速查表遇到的读者可以直接对号入座现象可能原因处置建议工具偶尔不生效模型版本旧工具调用不稳定升级模型增强系统提示约束工具返回 JSON 模型听不懂返回值嵌套过深工具内部裁剪字段后再返回知识库回答答非所问分块策略不合理按结构切块调整 chunk 参数检索结果不相关单一向量召回加 BM25 混合检索响应明显变慢旁路操作串行缓存 embedding异步化工具调用模型死循环调工具缺少轮数上限设置最大工具调用轮数我自己做 Agent 项目最大的体会是模型选型决定能力的上限知识与工具层决定能力的下限。见过不少团队把精力全花在调 prompt、换模型上却连工具的参数描述都写得含含糊糊知识库的文档更是直接扔进去就不管了。其实这两块做好了一个中等能力的模型也能把业务跑得很稳这两块偷懒了再强的模型也救不回来。最后分享一个小技巧无论工具还是知识库上线之后一定要留全链路日志——模型原始输出、工具输入输出、检索命中的文档块全部落盘。Agent 是个概率系统它的问题往往要回看模型当时到底看到了什么才能定位。保存好这份日志排查 Agent 相关 bug 的效率能提升一个量级。下一篇实战我会接着讲多智能体之间的消息路由与协作编排到时候见。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进