ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RuoYi-Vue-Plus 集成 AgentScope Java:构建纯 Java AI Agent 运行时

RuoYi-Vue-Plus 集成 AgentScope Java:构建纯 Java AI Agent 运行时

1. 为什么要在 RuoYi-Vue-Plus 里塞进一个 AI Agent 运行时

1.1 从"后台管理系统"到"AI 能力中台"的定位转变

RuoYi-Vue-Plus 这套脚手架在国内 Java 圈子里算是老熟人了。它把用户、角色、菜单、部门、字典、日志、代码生成这些后台管理系统的"标配零件"都给你备齐了,权限模型用的是 Sa-Token,数据层是 MyBatis-Plus,缓存是 Redisson,前端是 Vue3 + Element Plus。很多团队拿它当起点,两三天就能搭出一个能跑的后台。

但问题也随之而来:当业务方开始提"我要在系统里加个智能问答""我要让 AI 帮我自动填工单""我要做个能查数据库的对话助手"这类需求时,传统的做法是在 Controller 里直接 new 一个 HTTP 客户端,把请求转发给某个大模型接口,然后把返回的字符串丢回前端。这种做法在 Demo 阶段没问题,一旦要上生产,就会暴露出一堆麻烦:会话状态没地方存、工具调用(Function Calling)要手写解析、多轮对话的上下文管理全靠自己拼字符串、流式输出和前端 SSE 对不上、出错之后没法重试和追踪。

AgentScope 是阿里系开源的一套多智能体(Multi-Agent)框架,Python 版本用得比较多,后来也推出了 Java 版本。它的核心价值在于把"智能体"这个概念工程化了:一个 Agent 有明确的角色(System Prompt)、有记忆(Memory)、有可调用的工具(Toolkit)、有推理循环(Reasoning Loop),还支持多 Agent 之间的消息传递和协作。Harness 这个词在软件工程里通常指"测试脚手架"或"运行骨架",放到 Agent 语境下,我理解它指的是一套让 Agent 能够被托管、被调度、被观测的运行时外壳——你可以把它想象成给 Agent 套了一个"驾驶舱",让它能接入真实的业务系统,而不是孤零零地跑在一个 Python 脚本里。

把这两者结合,本质上是想解决一个很实际的问题:让 RuoYi-Vue-Plus 从一个"管理后台"升级成一个"AI 能力中台"。后台原有的用户体系、权限体系、数据权限、操作日志,全部可以复用给 AI 模块;而 Agent 运行时负责处理对话、工具调用、上下文管理这些 AI 特有的脏活累活。两者各司其职,边界清晰。

1.2 纯 Java 技术栈的执念与现实考量

有人会问:为什么不用 Python 写 Agent,然后 Java 通过 HTTP 调用?这个方案当然可行,而且很多团队就是这么干的。但它有几个绕不开的痛点。

第一是部署复杂度。你的运维团队本来只需要维护一套 Java 服务和一套 MySQL/Redis,现在突然要多维护一个 Python 运行时、一套 pip 依赖、一套独立的进程管理。如果公司规模大还好,小团队根本扛不住这种运维负担。

第二是数据一致性。Agent 要调用业务工具,比如"查询某个用户的订单",这个查询逻辑在 Java 侧已经有现成的 Service 和 Mapper,权限校验也在 Java 侧。如果 Agent 跑在 Python 里,要么通过 HTTP 回调 Java 接口(多一层网络开销和鉴权复杂度),要么在 Python 里重写一遍数据访问逻辑(数据权限就彻底失控了)。

第三是类型安全和调试体验。Java 的强类型在定义 Tool 的入参出参时优势明显,IDE 能给你补全、能给你报错。Python 虽然灵活,但在大型项目里,动态类型的坑踩起来是真的疼。

所以"纯 Java"这个选择,不是技术洁癖,而是在运维成本、数据一致性、开发体验三者之间做的权衡。AgentScope 的 Java 版本让这个选择变得可行——虽然它的生态成熟度还不如 Python 版,但核心的 Agent、Memory、Toolkit、Message 这些抽象都已经有了,足够支撑一个生产级的集成。

1.3 集成后的能力边界:能做什么,不能做什么

在动手之前,得先把预期管理好。这套集成方案能给你带来的是:

  • 对话式交互:用户在前端聊天窗口输入问题,后端 Agent 处理并流式返回。
  • 工具调用:Agent 能根据用户意图,自动调用你注册的 Java 方法,比如查数据库、调内部 API、生成报表。
  • 多轮上下文:会话历史存在 Redis 里,支持按会话 ID 恢复上下文。
  • 权限继承:Agent 调用工具时,能拿到当前登录用户的身份,工具内部可以做数据权限过滤。
  • 可观测:每次对话的输入输出、工具调用链路、耗时,都能落到日志表里。

它不能给你带来的是:

  • 开箱即用的"通用智能":Agent 的能力上限取决于你接的大模型和注册的工具,别指望装上去就能回答一切。
  • 零代码接入:注册工具、写 System Prompt、调优参数,这些活还是得干。
  • 无限并发:大模型接口本身有 QPS 限制,Agent 的推理循环也会消耗 token,并发上去了成本是实打实的。

把边界想清楚,后面的架构设计才不会跑偏。

2. 整体架构设计与模块拆解

2.1 分层设计:让 AI 模块和业务模块解耦

我在设计这套集成时,最核心的原则是AI 模块不能污染原有业务模块。RuoYi-Vue-Plus 本身的分层是 controller → service → mapper,AI 相关的代码如果直接混进去,后期维护会非常痛苦。所以我采用了独立模块 + 依赖倒置的方式。

整体分成四层:

层级模块名职责依赖方向
接入层ruoyi-agent-controller提供 REST/SSE 接口,处理鉴权、参数校验依赖应用层
应用层ruoyi-agent-application编排 Agent 执行流程,管理会话生命周期依赖领域层
领域层ruoyi-agent-coreAgent 定义、Tool 注册、Memory 管理依赖基础设施层
基础设施层ruoyi-agent-infra大模型客户端、Redis 存储、日志持久化无业务依赖

这样分的好处是:领域层完全不依赖 RuoYi 的任何东西,你可以把它单独抽出来做成一个 starter,将来换到别的脚手架也能用。应用层负责把 RuoYi 的登录用户、权限上下文注入到 Agent 执行环境里。接入层就是薄薄的一层 Controller,不写业务逻辑。

2.2 核心组件选型:AgentScope Java 版的关键抽象

AgentScope Java 版里我重点用到了这几个抽象:

Agent:智能体的核心载体。它持有 System Prompt、Memory、Toolkit 和 Model。你可以把它理解成一个"有状态的函数"——输入是用户消息,输出是 Agent 的回复(可能包含工具调用)。

Memory:记忆管理。默认有 InMemoryMemory,但生产环境肯定要用 Redis 或者数据库。Memory 的接口设计很关键,它要支持按会话 ID 存取、支持消息裁剪(token 超限时丢弃旧消息)、支持消息类型区分(用户消息、助手消息、工具调用消息、工具返回消息)。

Toolkit:工具注册中心。你把 Java 方法注册进去,Agent 在推理时就能"看到"这些工具的描述(名称、参数 schema、功能说明),然后决定是否调用。这里有个坑:工具的描述写得越清楚,Agent 调用得越准。我见过有人把工具描述写成"查询数据",结果 Agent 根本不知道该什么时候调。

Model:大模型客户端抽象。AgentScope 支持多种模型,我这边接的是兼容 OpenAI 协议的接口,这样换模型供应商时只需要改配置。

Message:消息对象。要特别注意区分 Msg 和 ToolUse、ToolResult 这些类型,序列化到 Redis 时如果类型信息丢了,反序列化会报错。

2.3 会话状态管理:Redis 存储结构设计

会话状态是这套系统里最容易出问题的地方。我踩过的坑包括:消息序列化失败、并发写覆盖、token 超限没裁剪、会话过期时间设置不合理。

最终我采用的 Redis 存储结构是这样的:

# 会话元信息,Hash 结构 agent:session:meta:{sessionId} - userId: 用户ID - agentName: 使用的 Agent 名称 - createTime: 创建时间戳 - lastActiveTime: 最后活跃时间戳 - messageCount: 消息数量 # 消息列表,List 结构,每个元素是一条消息的 JSON agent:session:messages:{sessionId} # 会话锁,防止并发写 agent:session:lock:{sessionId}

消息的 JSON 序列化我用的是 Jackson,并且开启了类型信息保留(activateDefaultTyping),否则 ToolUse 这种多态类型反序列化会变成 LinkedHashMap,Agent 拿到之后直接懵掉。

过期时间我设置的是 2 小时滑动过期,每次有新消息就刷新。这个值是根据业务场景定的——客服场景可能 30 分钟就够,知识问答场景可能用户会隔几小时回来继续问,2 小时是个折中。

注意:Redis 的 List 结构在消息很多时,读取全量消息会有性能问题。我的做法是只保留最近 N 条(比如 50 条)在 Redis 里,更早的消息归档到 MySQL,需要时再按需加载。这个 N 要根据模型上下文窗口和平均消息长度来算。

3. 核心细节解析与实操要点

3.1 Agent 定义:System Prompt 怎么写才有效

System Prompt 是 Agent 的"人格设定",写得好不好直接决定用户体验。我总结了几个原则:

第一,明确角色和边界。不要写"你是一个智能助手",要写"你是某公司的工单处理助手,负责帮助用户查询工单状态、创建工单、修改工单优先级。对于与工单无关的问题,礼貌拒绝并引导用户回到工单话题"。

第二,把工具的使用场景写清楚。Agent 不知道你的工具有什么用,你得告诉它。比如"当用户询问订单状态时,调用 queryOrderStatus 工具,参数是订单号。如果用户没提供订单号,先追问"。

第三,约定输出格式。如果你希望 Agent 返回结构化的内容,就在 Prompt 里说清楚。比如"回答时先给出结论,再给出依据,最后给出建议操作"。

第四,控制幻觉。加一句"如果工具返回的数据不足以回答问题,如实告知用户,不要编造"。

我实际用的一个 System Prompt 模板长这样:

你是{companyName}的{roleName},负责{responsibility}。 你可以使用以下工具: - {toolName}: {toolDescription} 工作原则: 1. 优先使用工具获取真实数据,不要凭记忆回答业务问题。 2. 工具调用失败时,向用户说明失败原因,不要假装成功。 3. 涉及用户隐私的数据,只在必要时查询,不要主动展示。 4. 回答保持简洁,避免冗长的客套话。 当前用户信息:{userContext}

注意{userContext}这个占位符,它会在运行时被替换成当前登录用户的昵称、部门、角色等信息,让 Agent 的回答更贴合上下文。

3.2 工具注册:把 Java 方法变成 Agent 能调用的 Tool

工具注册是这套集成里技术含量最高的部分。AgentScope 的 Toolkit 需要你提供工具的名称、描述、参数 schema 和实际执行逻辑。在 Java 里,我用了注解 + 反射的方式来自动化这个过程。

先定义一个注解:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AgentTool { String name(); String description(); boolean requireUserContext() default false; }

然后在业务 Service 的方法上打注解:

@Service public class OrderToolService { @AgentTool( name = "queryOrderStatus", description = "根据订单号查询订单状态。参数 orderNo 为订单编号,必填。", requireUserContext = true ) public OrderStatusVO queryOrderStatus(String orderNo, UserContext ctx) { // 这里可以基于 ctx 做数据权限过滤 return orderService.getStatusByNo(orderNo, ctx.getUserId()); } }

启动时,用一个 BeanPostProcessor 扫描所有带@AgentTool的方法,解析参数类型生成 JSON Schema,注册到 Toolkit 里。参数 schema 的生成我用的是 victools 的 jsonschema-generator,它能根据 Java 类型自动生成符合 JSON Schema 规范的描述。

这里有几个坑要提醒:

坑一:参数类型要简单。Agent 生成参数时是基于 JSON 的,所以你的方法参数最好是 String、Integer、Boolean 这些基础类型,或者简单的 POJO。如果你用了复杂的泛型嵌套,schema 生成会出问题,Agent 也容易生成错误的参数。

坑二:方法要幂等。Agent 可能会因为推理循环重复调用同一个工具,如果你的工具是"创建订单"这种非幂等操作,就会出大问题。我的做法是给非幂等工具加一个"确认机制"——Agent 先调用一个 dryRun 版本,返回"将要执行的操作",等用户确认后再调用真实版本。

坑三:异常要捕获。工具执行抛异常时,不要让异常直接冒泡到 Agent 循环里,否则整个对话就断了。应该捕获异常,返回一个结构化的错误信息,让 Agent 知道"这个工具调用失败了,原因是 XXX",它可以选择重试或者告知用户。

3.3 流式输出:SSE 与 Agent 推理循环的对接

前端聊天窗口需要流式输出,否则用户要盯着 loading 转好几秒。AgentScope 的 Agent 支持流式回调,但和 Spring 的 SSE 对接时要注意线程模型。

我的做法是用SseEmitter,在 Controller 里创建一个 emitter,然后提交一个异步任务去执行 Agent。Agent 的流式回调里,每产生一段文本就调用emitter.send()。这里的关键是回调线程和请求线程不是同一个,所以 emitter 的 send 要加同步控制,否则并发 send 会抛 IllegalStateException。

@PostMapping("/chat/stream") public SseEmitter chatStream(@RequestBody ChatRequest request) { SseEmitter emitter = new SseEmitter(180_000L); String sessionId = request.getSessionId(); UserContext ctx = SecurityUtils.getUserContext(); executor.execute(() -> { try { agentRuntime.runStream(sessionId, request.getMessage(), ctx, chunk -> { synchronized (emitter) { emitter.send(SseEmitter.event() .name("message") .data(chunk)); } }); emitter.send(SseEmitter.event().name("done").data("[DONE]")); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }

超时时间我设的是 180 秒,因为有些复杂问题 Agent 要调用多个工具,推理时间会比较长。如果你的模型响应慢,这个值还要往上调。

还有一个细节:SSE 的事件类型要区分。我把文本增量、工具调用开始、工具调用结束、最终结果分别用不同的事件名发出去,前端可以根据事件类型做不同的 UI 展示。比如工具调用时显示"正在查询订单...",让用户知道系统在干活。

3.4 上下文窗口管理:token 超限时的消息裁剪策略

大模型的上下文窗口是有限的,多轮对话聊久了肯定会超。我的裁剪策略是:

第一步,保留 System Prompt。这个永远不能丢。

第二步,保留最近 N 轮对话。N 根据模型窗口大小和平均消息长度动态计算。比如模型窗口是 8K token,System Prompt 占 500,工具描述占 1000,那留给对话历史的就是 6500。如果平均每条消息 200 token,那大概能保留 32 条消息,也就是 16 轮。

第三步,对工具调用消息做特殊处理。ToolUse 和 ToolResult 是成对出现的,裁剪时不能只保留一个,否则 Agent 会困惑。我的做法是把它们作为一个原子单元,要么都保留,要么都丢弃。

第四步,超长消息做截断。如果单条工具返回结果特别长(比如查询返回了 100 条记录),我会在工具层面就做限制,只返回摘要信息,详细数据让 Agent 需要时再查。

token 计算我用的是 jtokkit,它能按模型类型准确计算 token 数。不要用"字符数除以 4"这种土办法,中文和英文的 token 比例差很多,算不准会导致要么浪费窗口,要么超限报错。

4. 实操过程与核心环节实现

4.1 环境准备与依赖引入

先说版本。RuoYi-Vue-Plus 我用的是 5.x 版本,JDK 17,Spring Boot 3.x。AgentScope Java 版本迭代比较快,我用的是当时较新的一个稳定版。依赖引入要注意排除冲突,特别是 JSON 处理库和 HTTP 客户端。

<dependency> <groupId>io.agentscope</groupId> <artifactId>agentscope-core</artifactId> <version>${agentscope.version}</version> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency>

排除 Jackson 是因为 RuoYi 本身已经管理了 Jackson 版本,重复引入会导致版本冲突,序列化行为不一致。

配置方面,我在application.yml里加了这些:

agent: enabled: true default-model: qwen-plus models: qwen-plus: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${AGENT_API_KEY} timeout: 60000 max-tokens: 2048 temperature: 0.7 session: ttl: 7200 max-messages: 50 lock-timeout: 10

API Key 一定要走环境变量或者配置中心,不要硬编码在 yml 里,更不要提交到 Git。

4.2 Agent 运行时的初始化流程

Agent 运行时的初始化我放在了一个@Configuration类里,用@PostConstruct触发。流程是这样的:

  1. 读取配置,创建 Model 实例。
  2. 扫描所有@AgentTool注解的方法,注册到 Toolkit。
  3. 为每个业务场景创建 Agent 定义(不同场景用不同的 System Prompt 和 Toolkit 子集)。
  4. 把 Agent 定义缓存到一个 Map 里,key 是 agentName。

这里有个设计决策:Agent 是有状态的还是无状态的?AgentScope 的 Agent 对象本身持有 Memory,如果做成单例,多个用户的会话会串。我的做法是 Agent 定义(Prompt + Toolkit + Model)做成单例,但每次执行时创建一个轻量的 Agent 实例,Memory 从 Redis 加载。这样既复用了重资源,又保证了会话隔离。

@Component public class AgentRuntime { private final Map<String, AgentDefinition> definitions; private final SessionStore sessionStore; private final ModelFactory modelFactory; public String run(String sessionId, String userMessage, UserContext ctx) { AgentDefinition def = definitions.get("default"); Memory memory = sessionStore.load(sessionId); Agent agent = Agent.builder() .name(def.getName()) .sysPrompt(buildPrompt(def, ctx)) .model(modelFactory.get(def.getModelName())) .memory(memory) .toolkit(def.getToolkit()) .build(); Msg response = agent.call(userMessage); sessionStore.save(sessionId, memory); return response.getTextContent(); } }

4.3 工具调用的完整链路追踪

工具调用的链路追踪是我花时间最多的地方,因为出问题时如果没有日志,根本不知道 Agent 为什么调了某个工具、传了什么参数、返回了什么。

我在 Toolkit 外面包了一层装饰器,每次工具调用都记录:

public class TracingToolkit implements Toolkit { private final Toolkit delegate; private final AgentCallLogMapper logMapper; @Override public ToolResult call(String toolName, Map<String, Object> args, UserContext ctx) { long start = System.currentTimeMillis(); AgentCallLog log = new AgentCallLog(); log.setToolName(toolName); log.setArgs(JsonUtils.toJson(args)); log.setUserId(ctx.getUserId()); try { ToolResult result = delegate.call(toolName, args, ctx); log.setStatus("SUCCESS"); log.setResult(truncate(result.getContent(), 2000)); return result; } catch (Exception e) { log.setStatus("FAILED"); log.setErrorMsg(e.getMessage()); throw e; } finally { log.setCostMs(System.currentTimeMillis() - start); logMapper.insert(log); } } }

结果我做了截断,只存前 2000 字符,避免日志表被大字段撑爆。完整结果如果需要排查,可以另外存到对象存储。

这张日志表后来成了我调优的主要依据。通过分析日志,我发现了一些问题:某个工具被频繁调用但总是失败(参数 schema 描述不清)、某个工具从来没被调用过(描述写得太模糊)、某些对话工具调用次数异常多(System Prompt 没约束好)。

4.4 前端对接:聊天窗口的关键交互设计

前端这块我不展开讲 Vue 的细节,重点说几个交互设计上的坑。

第一,流式渲染要做节流。SSE 推送的频率可能很高,如果每来一个字符就 setState,React/Vue 的渲染压力会很大。我的做法是用一个缓冲区,每 50ms 刷新一次 UI。

第二,工具调用要有视觉反馈。当 Agent 在调用工具时,前端应该显示一个"正在执行 XXX"的提示,而不是让用户干等。我在 SSE 事件里加了tool_call_start和tool_call_end事件,前端据此显示/隐藏 loading。

第三,错误要友好。Agent 执行失败时,不要把堆栈信息直接展示给用户。我在后端做了错误分类:模型超时、工具失败、参数错误、系统异常,每类对应不同的用户提示文案。

第四,会话列表要能管理。用户可能同时开多个会话,前端要有一个侧边栏展示历史会话,支持切换、重命名、删除。会话的元信息从后端接口拉取。

第五,Markdown 渲染要防 XSS。Agent 返回的内容可能包含 Markdown,渲染时一定要用安全的渲染器,并且对 HTML 标签做转义。我见过有人直接v-html,结果 Agent 返回的内容里带了 script 标签,直接 XSS。

5. 常见问题与排查技巧实录

5.1 工具调用失败排查速查表

现象可能原因排查方法解决方案
Agent 不调用工具,直接回答工具描述太模糊查看 System Prompt 和工具描述补充工具的使用场景说明
工具调用参数缺失参数 schema 没标 required检查生成的 JSON Schema在注解里标注必填参数
工具调用参数类型错误参数类型复杂查看日志里的 args简化参数类型,用基础类型
工具调用后 Agent 不继续推理ToolResult 格式不对检查返回内容是否为空确保返回非空字符串
工具调用死循环工具返回结果让 Agent 困惑查看调用日志在 Prompt 里加"不要重复调用同一工具"
工具调用超时工具执行太慢查看 costMs加超时控制,或异步化

5.2 会话串号的排查思路

会话串号是个很隐蔽的问题,表现是 A 用户看到了 B 用户的对话内容。我排查过一次,原因是 Memory 的 key 生成逻辑有 bug——用了userId + agentName作为 key,但没加 sessionId,导致同一用户的不同会话共享了 Memory。

排查这类问题的思路是:

  1. 在 SessionStore 的 load 和 save 方法里打日志,打印 sessionId 和消息数量。
  2. 复现时观察日志,看两个不同会话的 sessionId 是否真的不同。
  3. 检查 Redis 里的 key,确认存储结构符合预期。
  4. 检查是否有地方直接用了 Agent 单例而没走 SessionStore。

修复之后我加了一个防御性检查:load 出来的 Memory 里,如果消息的 sessionId 和请求的 sessionId 不一致,直接抛异常并告警。

5.3 性能优化的几个实操点

第一,模型调用是瓶颈。一次对话可能要调用模型 2-3 次(推理、工具调用后的再推理),每次几百毫秒到几秒。优化方向是:能并行调用的工具并行调用、减少不必要的推理轮次、用更快的模型处理简单问题。

第二,Redis 读写要批量化。每次对话结束保存 Memory 时,不要一条条消息地写,用 pipeline 批量写。

第三,工具执行要加缓存。有些工具是查询类的,同样的参数短时间内重复调用可以走缓存。我用 Caffeine 做了一个本地缓存,TTL 设 30 秒,效果不错。

第四,SSE 连接要控制数量。每个 SSE 连接占用一个线程,如果并发用户多,线程池会被打满。我的做法是用 WebFlux 的响应式流替代 SseEmitter,但改造量比较大,小规模场景用线程池 + 合理配置也能扛。

5.4 成本控制的经验

大模型调用是按 token 计费的,如果不控制,账单会很吓人。我做了这几件事:

第一,设置单用户日限额。在 Redis 里记录每个用户每天的 token 消耗,超过阈值就拒绝服务并提示。

第二,缓存常见问题的回答。有些问题是高频的,比如"怎么修改密码",这类问题的回答可以缓存,命中缓存就不调模型。

第三,用便宜模型做意图识别。先用一个小模型判断用户意图,如果是简单问题直接走规则回答,复杂问题才走大模型。

第四,限制工具返回的数据量。工具返回的内容也会算进 token,所以查询类工具要分页,默认只返回前几条。

第五,监控异常消耗。我加了一个定时任务,每小时统计 token 消耗,如果某个用户或某个 Agent 的消耗异常高,发告警。

6. 一些踩坑之后的个人体会

这套集成做完之后,我最大的感受是:AI 集成不是技术问题,是工程问题。模型能力是现成的,AgentScope 也把框架给你了,真正难的是怎么让它稳定、可控、可观测地跑在生产环境里。

我见过太多团队在 Demo 阶段很兴奋,一上生产就各种问题:会话丢了、工具调错了、成本失控了、用户投诉了。这些问题都不是模型的问题,是工程没做到位。

如果让我给准备做类似集成的团队提建议,我会说:先把会话管理、日志追踪、错误处理这三件事做扎实,再去做花哨的功能。会话管理保证用户体验的连续性,日志追踪保证出问题能排查,错误处理保证系统不会因为一次模型超时就整体不可用。这三件事做好了,剩下的就是慢慢调优。

另外,不要迷信"全自动"。Agent 再智能,也需要人的兜底。我在设计时留了一个"转人工"的入口,当 Agent 连续两次无法解决用户问题时,自动提示用户可以转人工客服。这个设计让用户满意度提升了不少,因为用户知道"实在不行还有真人"。

最后说一个技术选型上的体会:AgentScope Java 版虽然能用,但生态确实不如 Python 版丰富。如果你的团队有 Python 能力,且不介意多维护一个服务,用 Python 版可能会更省心。但如果你的团队是纯 Java 背景,且对运维复杂度敏感,那 Java 版是更务实的选择。技术选型没有绝对的对错,只有适不适合你的团队和场景。

RELATED READING

延伸阅读

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