Skip to main content

基于AgentScope,如何构建Agent

上一篇走完选型,AgentScope Java 2.0在三个候选里更符合咱们的需求。把依赖加进pom,用ReActAgent.Builder几行代码就能建出一个Agent。比特严选那句话丢进去:

我上周买的AirPods Pro左耳突然没声音了,放回充电盒也没用,你帮我看看是不是坏了,要是修不好我想退。

循环真的转了三圈。查订单拿到签收时间,查当前日期算七天窗口,窗口还在,发起售后。和序章那段消息序列长得一模一样,一行循环代码都没写。

到这一步只能算跑通。真往生产上放,事情会一件件冒出来。聊到七八轮,上下文里堆满工具返回的大段文本,每轮请求带出去的东西比第一轮多好几倍。用户上周说过家里有一台HomePod,这周新开个会话就不记得了。模型决定发起售后,这是会真改业务数据的写操作,肯定不能不经过用户确认就执行。以及答错了想查是哪一轮走偏的,得有地方看到模型每一轮看见了什么、选了什么以及返回了什么等。

这些事AgentScope多数有对应的能力,但能直接拿来用的不多,另外有一部分是自己另建了一套。上下文怎么裁、记忆怎么分层、写操作前怎么确认、链路怎么追踪,每一件后面都有专门的篇目,这里先不展开。本篇回答更前面的问题:框架到底给了什么,Ragent用了哪些,又在它外面建了什么。

这篇文档会从宏观层面介绍AgentScope框架,以及咱们做了哪些增强优化,有个大概印象就好,具体细节后在后续文档中补充。

技术背景:AgentScope框架长什么样​

1. 定位与演进​

AgentScope Java是一个Agent运行时框架。上一篇选型那张表里写过它的时间线:2025年11月v0.2才具备核心ReActAgent能力,12月发1.0,2026年6月上2.0。十个月四个大版本,迭代节奏很快。

它和Spring AI、LangChain4j最大的区别在定位上。Spring AI给的是组件,Advisor、ChatClient、ToolCallingManager这些零件,由你自己拼出一个Agent循环。LangChain4j的AiServices加@Tool做到了声明式工具调用,但Agent编排能力仍在beta。AgentScope直接给了一个完整的ReActAgent类——循环骨架、工具执行、状态管理、中间件扩展,全在里面。

对应的代价也很直接:2.0移除了官方Spring Boot Starter,装配得自己来。这一点上一篇对接那几节讲过,不再重复。

官方网站:https://java.agentscope.io/v2/zh/docs,重点查看快速开始和核心组件章节即可。

下面这个是AgentScope 2.0的架构图,虽然看着很炫酷,但是绝大部分都是组件和HarnessAgent的概念,大家看看就好。

提供一张我让GPT画的结合Ragent的架构图,感觉会更清晰一些。

2. 核心架构​

AgentScope的设计可以拆成这几个核心概念。不需要全记住,后面提到的时候回来对就行。

概念是什么对应Ragent里的
ReActAgent内建ReAct循环的Agent抽象,通过Builder构建主Agent实例,由ReActAgentProvider管理
Toolkit工具注册容器,模型看到的工具清单从这里来AgentToolCatalog.buildToolkit()组装
MiddlewareBase中间件接口,五个拦截点,洋葱式执行七层中间件链,挂在Builder上
AgentStateStore状态持久化扩展点,把会话上下文序列化存取PgAgentStateStore,自己实现的PG版
RuntimeContext每次调用的运行时上下文,携带userId、sessionId和自定义属性编排层构建,一路透传到工具内部

这里面最值钱的是MiddlewareBase。上一篇说过,框架不是替你写那个while,而是把循环内部几个让出控制权的位置定成接口。中间件就是这些接口的统一载体。

3. ReActAgent转一圈发生了什么​

序章拆循环的时候说过,Agent比RAG多出来的只有一条从观察回到思考的回边。ReActAgent包掉的就是这一整条回边,外加围着它的几件杂事:

它负责的具体做了什么
推理把系统提示词、历史消息、工具schema拼成一次模型调用,流式拿回文本、思考和tool_calls
工具执行按tool_calls分发到Toolkit里注册的工具,一批多条时并发执行,结果按toolCallId回填
状态读写每次调用前从AgentStateStore加载这个会话的状态,跑完写回,内存里还带一层缓存
重试模型调用失败时按配置重新订阅
熔断转到maxIters还没收敛,追加一次总结调用收尾,并发出ExceedMaxItersEvent
事件流推理和工具执行的全过程以Reactor Flux<AgentEvent>的形式向外推送

前五行手写也能写出来,只是比较繁琐。最后一行是真正把框架和手写实现拉开距离的地方:整条执行链路是响应式的,每个阶段的事件(TextBlockDeltaEvent、ToolCallStartEvent、ToolResultEndEvent等)实时推出来,前端才能做到打字机效果和工具进度条。手写要把Reactor的上下文传播、背压、取消信号全处理对,比写循环本身难得多。

4. 中间件的五个拦截点​

MiddlewareBase接口在循环的五个位置各开了一个口子,每个对应一个on*方法:

  • onAgent:最外层,包住整次Agent调用的起止
  • onReasoning:每一轮推理的入口,能看到并修改即将发给模型的消息列表和工具清单
  • onModelCall:真正调模型API之前,能看到最终的请求参数(消息、工具定义、模型选项)
  • onActing:工具执行阶段,能看到模型选了哪些工具、拿到执行结果流
  • onSystemPrompt:直接改写系统提示词

前四个拦截点采用**“洋葱式”调用:请求逐层进入,结果反向返回。** 假设依次注册了中间件A、B、C,那么调用时先经过A,再经过B、C;内部执行完成后,结果再经过C、B、A返回。因此,先注册的中间件先处理输入、后处理结果。onSystemPrompt则不同:它不包裹后续调用,只接收当前的系统提示词,并返回修改后的文本。

多个中间件通过这种执行顺序配合工作,不需要互相调用。例如,可以让记忆中间件先把长期记忆插入消息列表,再让压缩中间件检查补充后的消息是否超过预算,并按规则压缩,最后交给模型。记忆层负责“补充内容”,压缩层负责“控制体积”,框架负责把它们串起来。它们不需要了解彼此的内部实现,但执行顺序需要安排好:先注入、再压缩,新增的记忆才会被纳入这一次预算检查;顺序反过来,这次检查就看不到后面才插入的内容。

Agent的设计放进了控制台​

框架的账下一节再算。这一节先交代一个不在框架里、却决定了后面好几个设计的事实:Ragent里一个Agent长什么样,不写在代码里,写在控制台里。

1. 通用智能体,差的只是几段话​

Ragent不是给某一家定制的客服,它是一个通用智能体。同一套代码,接上企业知识库就是内部员工助手,接上比特严选的商品、订单和售后数据就是电商助手。两边不一样的地方不在循环、不在工具桥接、也不在记忆和追踪,这些全是一套;不一样的是几段话:模型是谁、几类工具之间怎么选、知识库查出来的东西怎么组织成答案、会话太长了怎么压。这几段话决定了系统的行为,却不是代码。

1.1.0之前它们是resources/prompt/下的.st模板,改一个字要改源码、发版、重启。放到Agent档下这个别扭更明显:人设是所有行为的起点,调一个字模型的表现就可能变样,而最该动它的是懂业务的人,不一定是能提PR的人。所以2.0把面向终端用户的提示词整体搬进了数据库,控制台上开了“智能体管理”,一个智能体就是一份人设加一组提示词槽位。

提示词按用途拆成十二条槽位,1.x编排链路原有的几段和2.0随Agent档新增的几段摆在同一个页面上。槽位怎么分组、当前档位下哪几条真会被读、留空的那些内容从哪来,下一篇专门讲,本篇只用到其中一条结论。

2. 放进控制台有什么帮助​

第一件事是能看,也能改。

提示词其实是这个系统里最值得反复检查的东西。相比某一段Java代码,它往往更直接地影响模型最后怎么理解问题、怎么回答、怎么选工具。

如果提示词一直放在.st文件里,基本只有能拉代码、看工程的人才能检查。搬到控制台以后,产品、运营、测试都可以直接打开看;发现表述不合适,当场就能改,保存后从下一次会话开始生效,不需要重新发版。

当然这里提醒大家,线上修改提示词还是要慎重,需要经过大量的测评集回归才行。

第二件事是测试时可以直接切人设。

控制台允许新建多个智能体,但同一时刻只会有一个处于生效状态。所谓切换,本质上就是把当前生效的智能体换成另一个。

比特严选这套数据集就是这样接进去的:内置的默认助手一个字不动,另外建一个比特严选Agent,把它设为生效。需要切回企业助手时,重新激活内置那份即可。

代码不用换,进程不用重启,只切一份运行时配置。

3. 人设可切,能力边界不随人设走​

这里需要先明确一个边界:切换智能体,切换的只是人设和提示词,不会连带切换知识库和工具。

知识库、MCP工具这些能力都挂在意图树上,最终能查什么数据、能调用哪些工具,由意图树决定。

比如把当前智能体从“默认助手”切换成“比特严选Agent”,变化的是模型的表达方式,以及面对问题时如何判断和选择工具;但它能访问的知识库、可以调用的MCP工具并不会因此发生变化。

这样设计是有意为之。人设解决“怎么回答”,意图树解决“能做什么”。 两者分开配置,才能避免每换一个人设,都要重新配置一遍知识库和工具。

所以,一套完整的“比特严选”数据集并不只有人设,还包括知识库、意图树和技能手册。人设只是其中的一部分,而不是整个Agent的能力配置。

这件事放到本篇里,真正需要关注的是一个直接后果:创建ReActAgent所依赖的两项输入——人设文本和工具清单,都不是固定不变的,而是可能在运行期发生变化。

管理员修改并保存一次人设,或者在意图树上新增、移除一个工具,后续创建的Agent就应该使用最新配置。

这也直接决定了后面讲工厂构建的时候该怎么设计:它不能把人设和工具清单当成启动时一次性确定的静态配置。

项目里是什么样​

1. 构建ReActAgent有二十八个在途配置项,Ragent用了八个​

ReActAgent只能通过Builder构建。

我数了一遍agentscope-core 2.0.2里的ReActAgent.Builder:按方法名去重后,一共有38个返回Builder自身的public方法。

但这38个方法并不在同一个层面。

其中10个标记了@Deprecated(forRemoval = true),属于1.x遗留下来的兼容入口,源码里甚至专门划出了一块区域集中放置。真正属于2.0.2当前版本的配置项,其实只有28个。

这28个里,既有name、model这种构建Agent时最基础的配置,也有enableMetaTool、stopOnReject、skillCodeExecutionEnabled这类一看名字就知道背后牵着一整套运行机制的开关。

而Ragent实际只配置了其中8个。

也就是说,剩下20个当前有效的Builder配置项,我们一个都没有显式设置。

这恰好是理解这一篇最好的切入口。

这些配置不是我们没看到,也不是漏配了。相反,大部分我们都专门看过,只是最后没有采用框架给出的那套默认解法;其中还有几组能力,Ragent本身已经有自己的实现,再打开反而会形成两套机制同时生效。

至于真正用到的那8个参数为什么这么配、每一个具体控制什么,留到最后看代码时再逐个拆。

这里先把注意力放到另外那20个没有配置的选项 上:它们分别代表了AgentScope怎样理解Agent的能力边界,以及为什么Ragent最终选择不直接使用它们。

2. 记忆、知识库、技能​

没碰的那些配置项里,有三组需要着重说下,因为它们对应的正好是Agent最核心的三块能力:记忆、知识库、技能。三组加起来十四个配置项,全仓库搜一遍,零引用。

这一组框架的配置项状态Ragent的做法
记忆longTermMemory、longTermMemoryMode、longTermMemoryAsyncRecord2.0.0起标记待删除自研三层:裁剪、压缩、长期记忆
知识库knowledge、knowledges、ragMode、retrieveConfig2.0.0起标记待删除RAG收成一个普通工具search_knowledge
技能skillRepository、skillRepositories、skillFilter、dynamicSkillsEnabled、skillCodeExecutionEnabled、skillWorkDir(skillBox是1.x遗留)当期API手册存库,加载走工具调用,加载后遮蔽工具

表里前两组的状态那一列先解释一下。记忆和知识库那两组,框架自己标了@Deprecated(forRemoval = true, since = "2.0.0"),源码里把它们归在一段注释为“1.x legacy compatibility setters”的区域,javadoc写的是长期记忆正围绕新的基类重新设计、RAG也在重做。所以这两组不存在该不该用的问题,它们是等着被删的旧路。技能那组不一样,是2.0的当期API,没用它另有原因。

2.1记忆:入口待删,能力自研​

AgentScope记忆入口待删,并不意味着框架没有记忆能力。

io.agentscope.core.memory包下的StateBackedMemory仍然是当前版本的一部分,会话内消息的读取与写入一直依赖它。我们没有采用的,只是框架提供的那几个记忆配置入口,而不是绕开了整个Memory体系。

真正需要单独解决的是跨会话长期记忆。

例如,用户曾经说“家里已经有一台HomePod”,两个月后又说“那台已经送人了”。这时系统不能简单地把两条信息同时保留下来,也不能直接把旧记录物理删除。更合理的做法是:让旧记忆失效,由新信息覆盖当前事实,同时保留完整变更轨迹,能够追溯这条记忆最初来自哪一次对话、后来又在哪一轮被更新。

这已经超出了普通会话消息存储的范畴,本质上是一套带有生命周期、冲突处理和来源追踪的长期记忆机制,因此这部分采用了自研实现。

最终,比特严选的记忆体系由三层能力共同组成:上下文裁剪、会话压缩和跨会话长期记忆。三者分别解决不同层次的问题,并不是互相替代关系。

具体的设计取舍、触发条件以及实际测试结果,会放在后面的上下文与记忆章节中展开。

2.2知识库:不是能力不够,是时机不对​

1.0已经形成了一条完整的检索链路:向量、关键词和图谱三路并行召回,再经过混合排序、证据闸门和来源追踪。它的核心价值,恰恰在于这些围绕业务场景定制的检索细节。

如果接入框架内置的RAG抽象,检索结果需要先转换成框架统一的文档对象。但这层通用抽象无法完整承载Ragent现有链路中的关键信息,例如:证据来自哪篇文档、具体命中了哪个分块、通过哪条召回通道获得,以及对应的检索得分。

一旦转换成通用文档对象,这些信息就会被不同程度地抹平。这样损失的不只是一些元数据,更重要的是完整的证据链:后续模型无法准确知道一段内容是怎么被召回的,答案中的引用角标也就失去了可靠依据。

因此,这里没有改造原有RAG链路去适配框架抽象,而是选择保留现有实现,只在最外层增加一层工具封装,将整条检索链路作为普通工具search_knowledge暴露给模型。

这样既能继续复用1.0已经验证过的检索、排序和来源追踪能力,又能让它自然融入Agent的Tool调用体系,而不需要为了适配框架重新设计内部检索模型。

更完整的取舍和设计过程,会在后面讲解RAG如何变成Tool的文章中单独展开。

解锁付费内容,👉戳​