最近在搭一个内部工具的时候,我明显感觉到一件事:现在做AI应用,难点已经从"怎么调通一个大模型接口"变成了"怎么让多个大模型各自干好自己的活,还别互相打架"。单模型时代,你写一个超长的Prompt,把角色、上下文、要求全塞进去,模型勉强能输出个结果。但一旦业务变复杂,要拆成"需求理解、方案设计、代码生成、代码评审"好几个环节,再用单个Prompt硬刚,很快就会陷入上下文爆炸和指令混乱。
我试过用代码自己编排多轮对话,结果维护成本高得离谱。后来接触到AgentScope Java,才发现多智能体框架把这件事抽象得有多舒服。AgentScope Java是面向Java生态的多智能体开发框架,核心思路是把每个大模型调用封装成Agent,用消息作为它们之间通信的唯一载体,再用Pipeline或者更灵活的图结构把它们串起来。今天这篇就从一个Java开发者的视角,聊聊我这几周的实际使用体验、编程模型的细节,以及那些文档里没写清楚的坑。
1. 从单模型调用到多智能体协作:AgentScope Java到底解决了什么
1.1 单体应用里的"伪协作"
在聊框架之前,先复盘一下我自己以前是怎么写多智能体应用的。最常见的做法是定义一个枚举角色,然后循环调用同一个大模型接口,每次在Prompt里切换角色:
String prompt = "你现在是一个需求分析师。请分析以下需求..."; String response1 = callLlm(prompt); String prompt2 = "你现在是一个Java架构师。基于上一步的需求分析:" + response1 + ",请设计系统方案..."; String response2 = callLlm(prompt2); // 继续传递...这种"接力棒"式代码最大的问题是,所有上下文都靠字符串拼接,角色切换只是换了一层Prompt模板。一旦环节多了,你根本分不清某段话是哪个Agent说的、是事实还是模型幻觉。而且整个过程是同步阻塞的,哪怕两个Agent之间没有依赖关系,也没法并行执行。
还有个更隐性的问题:你没法给某个Agent单独指定不同的模型参数。比如需求分析用低延迟的模型就够了,代码评审却需要更强推理能力的模型。在单体循环里,你要么全用同一个模型,要么就得写一堆if判断去分流,代码丑到不想看第二遍。
1.2 多智能体框架的核心使命
AgentScope Java要做的事情,就是把上面这摊子事规范化。它提供了一套标准化的抽象,让开发者不需要关心消息是怎么传递的、Agent之间是怎么发现的、并行调度是怎么做的,你只需要描述"有哪些Agent,它们之间是什么关系"。
我理解它的核心价值有三个:
第一,Agent成为一等公民。每个Agent封装了自己的模型配置、系统Prompt、记忆状态和工具能力。后续想替换某个环节的模型,只需要改Agent的配置,调用方完全无感。
第二,消息驱动通信。Agent之间不直接调用方法,而是通过Message对象传递。这样天然支持异步、广播、订阅等复杂拓扑。消息里不仅带着文本内容,还带着sender、receiver、msgType等元数据,就像给每个发言都盖了个章。
第三,编排与执行分离。你定义好Agent之间的依赖关系(Pipeline或Graph),框架负责决定哪个Agent先跑、能不能并行、失败怎么重试。业务代码里不用写线程池,更不用手写状态机。
所以,AgentScope Java不只是一个"大模型调用封装库",它更像是一个面向AI应用的操作系统内核。你用Java写业务一样,自然地把大模型当作可调度的计算单元。
2. AgentScope Java的编程模型:Agent、Message与可组合的Execution
2.1 一切皆消息
AgentScope Java的API设计参考了Actor模型,但又比Actor更贴近AI场景。先看几个核心概念:
- Agent:一个独立的智能体,内部持有大模型配置、Prompt模板、记忆槽位。它接收Message,返回Message。
- Message:Agent之间的通信单元。字段包括msgId(消息唯一ID)、sender(发送者)、receiver(接收者)、content(内容,可以是文本或结构化数据)、msgType(业务类型)、timestamp。
- Pipeline:一种线性执行流,按顺序把消息从一个Agent传给下一个Agent。
- Execution:更通用的执行单元,可以定义条件分支、并行分支,甚至可以嵌套Pipeline。
我在第一次看源码的时候,发现它把消息设计得异常轻量。它没有强行要求所有消息都是字符串,content可以放一个Java对象,比如Map或者自定义DTO。这一点对Java开发者太友好了,因为真实业务里往往需要在Agent之间传递结构化数据,而不是一坨需要二次解析的文本。
2.2 用代码说话:一个最小Agent
来看一个最简单的Agent定义。假设我们有一个"翻译官"Agent,专门负责把中文翻译成英文:
@Agent public class TranslatorAgent extends LlmAgent { @Override protected void setUp() { this.model = ModelConfig.builder() .model("qwen-plus") .apiKey(System.getenv("DASHSCOPE_API_KEY")) .temperature(0.3) .build(); this.systemPrompt = "你是一名专业的中英翻译。只输出翻译结果,不要任何解释。"; } @Override public Message onMessage(Message input) { Message result = super.onMessage(input); return Message.builder() .sender("translator") .receiver(input.getSender()) .content(result.getContent()) .msgType("translation") .build(); } }这里有两个地方值得细品:一是用@Agent注解标记类,框架启动时自动扫描注册;二是super.onMessage(input)直接调用父类LlmAgent的默认逻辑——把输入消息拼进Prompt,请求大模型,返回响应。你不需要自己拼完整Prompt,框架会按照systemPrompt + 历史消息 + 当前输入的顺序组装。
我还试过把多个历史消息塞进去:默认情况下,LlmAgent会把当前收到的这条消息作为最新用户输入,历史消息从哪来?答案是框架有一个AgentMemory组件,默认提供最近N轮的滑动窗口。这让Agent在逻辑上"记得"之前的对话,不用每次手工拼接。
2.3 从Agent到Pipeline的编排能力
定义好单个Agent后,编排才是重点。Pipeline的用法非常直观:
Pipeline pipeline = Pipeline.builder() .addStep(requirementAgent) .addStep(designAgent) .addStep(codingAgent) .addStep(reviewAgent) .build(); Message finalResult = pipeline.execute( Message.builder() .content("实现一个用户登录接口,要求支持JWT") .msgType("task") .build());这段代码的意思是,原始任务消息依次经过需求分析、方案设计、代码生成、代码评审四个Agent。前一个Agent的输出会成为后一个Agent的输入。
真正让我觉得爽的是,Pipeline的每个步骤可以并行。比如需求分析做完之后,"前端方案设计"和"后端架构设计"这两个Agent完全可以同时跑。你可以这样写:
Execution parallel = Execution.parallel( frontEndDesignAgent, backEndDesignAgent ); Pipeline pipeline = Pipeline.builder() .addStep(requirementAgent) .addStep(parallel) .addStep(reviewAgent) .build();框架内部会为并行分支创建独立的线程池,等所有分支都返回后,再把合并结果传给下一步。在Java生态里,这种声明式并发比手写CompletableFuture要清晰得多。
3. 实操:搭建一个"需求分析-代码生成-代码评审"流水线
3.1 工程结构与依赖准备
纸上谈兵没意思,下面给一个可以直接跑起来的最小工程。我用的是Maven,Java 17,Spring Boot 3.2作为宿主应用。
首先引入依赖:
<dependency> <groupId>com.alibaba.agentscope</groupId> <artifactId>agentscope-java-core</artifactId> <version>0.1.0</version> </dependency> <!-- 如果你已经用了Spring Boot,还要引入集成包 --> <dependency> <groupId>com.alibaba.agentscope</groupId> <artifactId>agentscope-java-spring-boot-starter</artifactId> <version>0.1.0</version> </dependency>工程结构我按Agent分包:
com.example.demo ├── AgentApplication.java ├── agent/ │ ├── RequirementAgent.java │ ├── CodeGeneratorAgent.java │ └── CodeReviewerAgent.java └── config/ └── AgentRegistryConfig.java启动类不需要什么特殊配置,只要确保配置了模型API Key。如果你是国内云厂商的模型,用环境变量DASHSCOPE_API_KEY;如果你接的是国外模型,可以在application.yml里统一配置:
agentscope: llm: default: model: qwen-plus api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyun.com/compatible-mode/v13.2 三个Agent的代码实现
第一个Agent负责分析需求。它的systemPrompt要求把用户模糊的需求转成结构化条目。
@Agent public class RequirementAgent extends LlmAgent { @Override protected void setUp() { this.systemPrompt = """ 你是资深需求分析师。 将用户输入的需求拆分成:功能点、边界条件、性能要求、验收标准。 输出格式为Markdown列表,不要多余解释。 """; this.model = ModelConfig.builder() .model("qwen-plus") .temperature(0.2) .build(); } }第二个Agent是代码生成器。它要接收RequirementAgent的输出,然后写出实现代码。注意,它需要理解上一个Agent的格式约定。
@Agent public class CodeGeneratorAgent extends LlmAgent { @Override protected void setUp() { this.systemPrompt = """ 你是资深Java后端工程师。 根据需求分析结果,使用Spring Boot实现代码。 只输出关键类和接口的代码,包含OOP设计,不要输出解释说明。 """; this.model = ModelConfig.builder() .model("qwen-max") .temperature(0.4) .maxTokens(4096) .build(); } }第三个Agent是代码评审。它会挑毛病并给出修改建议。
@Agent public class CodeReviewerAgent extends LlmAgent { @Override protected void setUp() { this.systemPrompt = """ 你是一位严格的代码评审专家。 重点检查:安全性、并发问题、异常处理、代码风格。 对每一条问题给出严重级别:CRITICAL / MAJOR / MINOR。 如果没问题,输出"通过"。 """; this.model = ModelConfig.builder() .model("qwen-max") .temperature(0.1) .build(); } }三个Agent看起来都很简单,因为复杂度被框架消化了。LlmAgent的默认行为就是"把输入消息发给大模型,把响应包装成Message返回"。
3.3 运行结果与Message流转细节
写一个main入口:
@Component public class AgentRunner implements ApplicationRunner { @Resource private Pipeline demoPipeline; @Override public void run(String... args) { Message userTask = Message.builder() .sender("user") .content("实现一个带Redis缓存的用户详情查询接口,要求防止缓存穿透") .msgType("task") .build(); Message result = demoPipeline.execute(userTask); System.out.println(result.getContent()); } }运行后,终端会打印出评审意见。我截取一次输出的核心片段:
- CRITICAL: 缓存穿透保护不完整。
null值不应直接放行,应缓存空对象并设置短TTL。- MAJOR:
@Transactional与Redis操作不在同一事务内,缓存更新失败时数据不一致。- MINOR: 建议使用
Record定义DTO,减少样板代码。
你会发现,这几个Agent之间产生的消息,msgType始终是"task",但content从业务需求变成了评审意见。在正式项目里,你可以给每个环节定义不同的msgType,然后在Pipeline步骤里根据msgType决定走哪个分支,这就很灵活了。
4. 进阶玩法:记忆、工具调用与业务系统集成
4.1 给Agent加上短期记忆
默认的LlmAgent虽然能基于历史消息做多轮对话,但它是无状态的——每次执行Pipeline都会从干净的上下文开始。如果我们的场景是"客服机器人持续对话",这就需要为Agent配置记忆存储。
AgentScope Java提供了AgentMemory接口。你可以用一个简单的MemoryAgent封装:
@Agent public class ChatAgent extends LlmAgent { @Override protected void setUp() { this.memory = new SlidingWindowMemory(10); this.systemPrompt = "你是客服助手,回答尽量简洁。"; this.model = ModelConfig.builder() .model("qwen-plus") .build(); } }SlidingWindowMemory(10)表示只保留最近10条消息。消息会在调用大模型时自动注入到Prompt里。我自己实测下来,这种滑动窗口比全量历史的效果好——至少不容易让模型迷失在超长上下文里。如果你要做跨会话的持久化记忆,可以把AgentMemory的实现换成基于Redis或数据库的版本,在onMessage里先加载历史,再调用super.onMessage。
4.2 让Agent调用外部工具
很多场景下,Agent不能只靠大模型自身的知识,得让它"动手做点事",比如查数据库、调REST接口、执行一段代码。AgentScope Java的工具调用机制设计成了函数注册模式。
官方推荐定义一个工具类,用注解标注每个方法:
@Component public class OrderTools { @AgentTool(name = "query_order", desc = "根据订单号查询订单状态") public String queryOrder(String orderId) { return orderMapper.selectStatus(orderId); } @AgentTool(name = "cancel_order", desc = "取消指定订单") public String cancelOrder(String orderId) { orderService.cancel(orderId); return "订单已取消"; } }然后在Agent里绑定这个工具类:
@Agent public class OrderAssistantAgent extends LlmAgent { @Override protected void setUp() { this.toolManager = new ToolManager.OrderToolManagerBuilder() .add(new OrderTools()) .build(); this.systemPrompt = "你是电商订单助手,查询订单后务必调用工具获取真实数据,不要凭空猜测。"; } }框架会在调用大模型时,把工具列表包含进请求(兼容OpenAI的function calling格式)。模型如果认为需要调用工具,返回的响应里会带有function_call信息,框架自动执行对应Java方法,把结果作为新的消息再次返回给模型"反思",最终生成面向用户的回复。整个过程对开发者来说是透明的。
这里有个很小的细节:工具方法名在AgentScope Java里会被映射成API名称,比如query_order。如果方法名跟系统关键字冲突,记得用name属性显式指定。我第一次写的时候,工具方法叫delete,结果跟框架的默认操作冲突,排查了半天。
4.3 与Spring Boot的集成姿势
一般项目不会裸写main方法,而是把AgentScope Java嵌在Spring Boot工程里。官方提供的Spring Boot Starter做得比我预想中顺滑。
你只需要做三件事:
第一,在配置类里声明Pipeline。为了保证Agent之间的依赖关系清晰,我习惯用@Configuration统一装配:
@Configuration public class AgentRegistryConfig { @Bean public Pipeline demoPipeline(RequirementAgent requirementAgent, CodeGeneratorAgent codeGeneratorAgent, CodeReviewerAgent reviewAgent) { return Pipeline.builder() .addStep(requirementAgent) .addStep(codeGeneratorAgent) .addStep(reviewAgent) .build(); } }第二,确保Agent本身是Spring Bean。@Agent注解默认会交给Spring管理,所以你可以在Agent里用@Autowired注入Mapper、Service等。也就是说,Agent不再是大模型的"提词器",它可以直接接触业务数据。
第三,框架默认会在应用启动时扫描所有@Agent注解的Bean,并进行初始化。如果初始化时报错,多半是API Key没配置或者模型名填错了,建议通过agentscope.llm.default.model统一管理,而不是在每个Agent里硬编码。
我还试过把Pipeline的执行入口暴露成REST接口:
@RestController @RequestMapping("/ai") public class AiTaskController { @Resource private Pipeline demoPipeline; @PostMapping("/run") public Message run(@RequestBody String task) { return demoPipeline.execute(Message.builder() .sender("api") .content(task) .msgType("task") .build()); } }这种模式下,前端只需要把用户需求POST过来,后端整个多智能体流水线异步处理完再返回结果。对业务系统来说,它就是一个普通的服务接口。
5. 我在实战中踩过的几个坑
5.1 并发调用的线程模型问题
框架虽然方便,但默认的线程池配置需要根据场景调优。Pipeline并行模式下,如果你有4个并行Agent,而模型API有每秒并发限制,很容易触发限流。我一开始没注意,直接并行调了8个Agent,结果DashScope返回了429 Too Many Requests,Pipeline里一堆重试日志。
解决方法是显式配置并发度。你可以给Pipeline设置一个信号量或线程池:
Pipeline pipeline = Pipeline.builder() .executor(Executors.newFixedThreadPool(4)) .addStep(...) .build();更重要的一点是,模型调用本身是IO密集型操作,不要为每个Agent单独分配线程。让所有Agent共享一个小的IO线程池,配合内部重试,效果更好。
5.2 消息序列化的兼容性
AgentScope Java使用JSON序列化Message。如果content字段放的是自定义对象,该对象必须实现Serializable,最好也配置好Jackson注解。
我遇到过一个很典型的坑:用LocalDateTime作为content的一部分,序列化后是数组格式,反序列化时直接报错。后来在对象字段上加了@JsonFormat才算解决。所以如果你要在Agent之间传结构化数据,强烈建议定义独立的DTO,并控制好字段类型,别图省事直接塞Map——一旦有嵌套泛型,序列化很容易丢类型信息。
5.3 不同大模型接口的差异适配
AgentScope Java在设计上兼容OpenAI协议和国内几家主流的模型协议。但不同模型在能力边界上差别很大,主要体现在三个方面:
最大上下文长度:有的模型支持32K,有的只支持8K。如果Pipeline里的消息历史较长,需要根据模型动态调整滑动窗口大小。我在一个文档总结场景里,历史消息超过20轮,模型直接开始答非所问。后来把SlidingWindowMemory改成按字符长度截断,而不是按条数,才稳定下来。
工具调用的响应格式:虽有兼容层,但某些模型返回的tool_calls格式跟标准不完全一致。AgentScope Java内部做了容错,但如果你接到自定义模型,建议在测试阶段多跑几个工具调用用例,确认大模型真的能正确发起调用请求,而不是只输出一段JSON文本。
temperature的语义:不同厂商对temperature的解释有细微差异。AgentScope Java做了默认映射,但最好在使用前阅读目标模型文档,不要拿OpenAI的参数习惯去卡国内模型。
除了这些,还有一个容易忽略的点:AgentScope Java的LlmAgent默认会在Prompt前面加上systemPrompt,如果你的systemPrompt本身很长,再加上历史消息,很容易顶到模型的上下文上限。建议把长指令拆成systemPrompt和fewShot两个部分,让框架分开展示。
最后再分享一个调试技巧。我开发时从来不直接跑完整Pipeline,而是单独跑每一个Agent,用预设的Message输入,观察它的输出格式。你会很直观地发现某个Agent是不是总改格式、某个Agent是不是输出废话。等每个Agent都稳定了,再组装Pipeline,定位问题会快很多。这大概是使用多智能体框架最省时间的习惯。