第二章 从0搭建企业级HarnessAgent项目-基于AgentScope Java进行工程化改造
【阶段二】基于AgentScope Java 2.0.0 把 LingNova 升级成Harness Agent
这是 LingNova 企业级 Agent 项目的第二篇阶段性记录。阶段一我先用 Spring AI 跑通了对话、工具调用、Redis 窗口记忆和指标采集;阶段二想解决的,是另一个更现实的问题:当对话变长、任务变复杂、服务发生重启时,Agent 怎么还能够稳定地继续工作?
文章目录
一、为什么要引入Harness
阶段一跑下来其实挺顺手,DeepSeek + Spring AI 的组合写一个对话接口非常快,Redis 里塞 20 条消息也能撑住短对话。但只要把场景稍微往真实业务推一下,几个问题就藏不住了:
- 重启即失忆:Redis 里那 20 条消息丢了或者服务重新部署,用户上一句"我在汽车厂做焊接产线"相关的执行状态、任务进度全没了,又得重新交代一遍背景。
- 上下文越聊越长:用户连问 30 轮,Prompt 一直在膨胀,Token 账单肉眼可见地往上涨,而真正有价值的信息可能就那两三条。
- Agent 不知道自己该干啥:复杂任务(比如"帮我对比三款焊接机器人再写个选型报告")它一边想一边干,做到一半就跑偏,没有任何规划环节。
- 没有跨会话的记忆:用户上周说他预算 50 万,这周开个新会话过来,Agent 完全不记得这件事。
这些问题的共同点是:它们都不是模型能力问题,而是模型外面的那套系统没准备好。
"Harness"它不是某个具体框架的名字,而是一种工程思维:模型之外的那整套环境(工具、记忆、文件系统、编排、约束机制)才是决定 Agent 上限的东西。
所以阶段二的核心动作就是:给 LingNova 搭一套真正的 Harness。
二、Harness 是什么?
网上讲 Harness 的文章容易上来就抛一堆术语,什么"上下文工程"、“中间件链”、“沙箱执行环境”,其实一句话就能说清楚:
Harness = 模型之外的所有东西。
打个比方,模型是 CPU,Harness 就是操作系统。CPU 再强,操作系统天天崩,电脑也用不下去。你能让 Agent 记住多轮对话、能让它调用业务工具、能限制它别乱执行 shell 命令、能在它犯错时打回重做——这些"机制"加起来就是 Harness。
阶段一我们其实是手写了一个简陋版 Harness:用 Redis 存 20 条消息、用 Advisor 做日志和指标、用 Spring AI 的 Tool 注解暴露业务 API。能用,但是每一项都得自己维护,长会话、长任务、复杂编排很快就力不从心。
到了阶段二,我决定不再自己造轮子,直接引入专门的 Harness 框架。这就要提到 AgentScope Java。
三、引入AgentScope Java 2.0.0 GA:7 月刚发布的工程化框架
1、为什么选它
- 它是阿里出品,对Java友好,国内文档、issue 响应速度有保障,我这种中文母语开发者看起来不费劲。
- 它明确把自己定位成"Harness 框架",不是"又一个 Agent SDK"。它的核心抽象
HarnessAgent就是为长跑、状态恢复、工程化部署设计的。 - 引入方便,只需要JDK17就可以使用,在pom中配置也很方便。
关于 GA 版本,官方发布说明里有一段话:
AgentScope Java 2.0.0 正式发布(General Availability)。这是从 1.x 到 2.0 的首个正式版本,标志着 AgentScope Java 从"透明开发"迈向"系统工程"的里程碑。
“从透明开发迈向系统工程”——这句话基本概括了 2.0 的核心设计:不再让开发者手写记忆、手写会话、手写工具注册,而是把这些能力下沉到框架里,业务代码只关心"我要做什么 Agent、给它什么工具、让它怎么协作"。因此,AgentScope Java 2.0.0 能够让Spring后端项目更好的搭建Harness工程。
2、 AgentScope Java 2.0 的核心设计要点
挑几个对阶段二最有用的:
- 双层 Agent 架构:底层是
ReActAgent(无状态的推理核心,负责"推理 → 工具调用 → 回复"循环),上层是HarnessAgent,通过 Middleware 和 Toolkit 两个扩展通道叠加工作区、记忆、子 Agent、技能、计划模式等工程能力。核心推理循环原样保留,只叠加不替换——这个设计让我很安心,不会因为我加了个新组件就把原有逻辑搞坏。 - Workspace 工作区抽象:人格、知识、技能、会话日志全部用磁盘上的 Markdown/JSON 表达,每轮自动注入 system prompt。这意味着 Agent 的"大脑"是可见、可改、可备份的文件,而不是一坨黑盒对象。
- 状态持久化:
AgentStateStore接口支持内存、JSON 文件、Redis、MySQL、PostgreSQL 五种后端,按(userId, sessionId)自动分桶。Session 跨副本恢复、滚动发布不丢上下文都靠它。 - 中间件机制:五个阶段(
onAgent/onReasoning/onActing/onModelCall/onSystemPrompt)的洋葱模型,日志、安全、压缩、记忆 flush 这些横切逻辑各居其层,互不打架。 - 企业级分布式部署:一键配置 Redis / OSS / MySQL 作为分布式后端,多副本共享状态。
3、在 Java 工程里引入
依赖很简单,在 pom.xml 里加几行:
<agentscope.version>2.0.0</agentscope.version>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-core</artifactId>
<version>${agentscope.version}</version>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-harness</artifactId>
<version>${agentscope.version}</version>
</dependency>
<!-- 用 Redis 存 Session -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-redis</artifactId>
<version>${agentscope.version}</version>
</dependency>
<!-- 用 OpenAI 兼容接口接 DeepSeek -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-model-openai</artifactId>
<version>${agentscope.version}</version>
</dependency>
这里有个重要避坑点:2.0.0 GA 把 OpenAI、Anthropic、DashScope 等模型提供商从 agentscope-core 拆分出去了,必须单独引入 agentscope-extensions-model-openai。我一开始以为 core 包自带,结果代码里 OpenAIChatModel 死活导入不进来,折腾了半天才发现这个变更。
四、阶段二相比阶段一做了哪些改变
先列个对照表,再说细节:
| 维度 | 阶段一 | 阶段二 |
|---|---|---|
| Agent 核心 | Spring AI ChatClient | AgentScope HarnessAgent |
| 会话状态 | Redis 存 20 条消息(手写) | AgentStateStore + RuntimeContext(框架托管) |
| 多轮上下文 | 窗口截断,超了就丢 | Compaction 压缩 + 大结果卸载 |
| 长期记忆 | 无 | 双层 MEMORY.md + 每日流水账 |
| 子 Agent | 无 | Supervisor 模式 + 3 个专业子 Agent |
| 路由 | 直接调 ChatClient | HarnessGateway Channel 路由 |
| 工具生态 | Spring AI @Tool | @Tool + MCP Server + 工具白名单 |
| 业务知识 | 写死在 Prompt 里 | SKILL.md 技能包,运营可配置 |
| 任务规划 | 无 | Plan Mode 计划模式 |
| 文件系统 | 直接读写宿主机 | 可插拔(Local / Docker 沙箱) |
核心变化是:从"我自己拼一套机制"变成"框架提供机制,我只写业务"。听起来像退步,其实是进步——把记忆、压缩、会话恢复这些通用逻辑交给框架,业务代码就只剩下"这个 Agent 是干嘛的、能调哪些工具、子 Agent 怎么分工"这种业务描述。
五、HarnessConfig:一个文件看懂整个架构
阶段二所有核心配置都集中在一个 HarnessConfig.java 里。这个文件现在大概 500 行,但逻辑很清晰:每个 @Bean 对应一个 Harness 能力。下面逐个拆开讲。
1. 状态持久化:让会话在重启后还能接着聊
AgentStateStore 存的是 Agent 的运行状态,而不只是几条文本消息。这里选择 Redis,是因为项目已经具备 Redis 基础设施,且它适合做跨进程、跨副本的会话状态共享。
@Bean
public AgentStateStore agentStateStore() {
JedisPooled jedis = createJedisPooled();
return RedisAgentStateStore.builder()
.jedisClient(jedis)
.keyPrefix("lingnova:agent:session:")
.build();
}
这里有个坑要提醒:Jedis 5.2.0 的 API 变了,原来 new JedisPooled(host, port) 然后调用 auth() / select() 的写法直接报错。必须用 DefaultJedisClientConfig.builder() 把密码和 database 配进去,再通过构造函数传进去:
private JedisPooled createJedisPooled() {
DefaultJedisClientConfig.Builder configBuilder = DefaultJedisClientConfig.builder()
.database(redisDatabase);
if (redisPassword != null && !redisPassword.isBlank()) {
configBuilder.password(redisPassword);
}
JedisClientConfig config = configBuilder.build();
return new JedisPooled(new HostAndPort(redisHost, redisPort), config);
}
这种依赖库的 API 变更其实挺常见的,但调试起来很费时间。教训就是:升级依赖之前先看一眼 release notes,别一上来就 mvn clean install。
2. 上下文压缩:CompactionConfig
阶段一的 20 条窗口解决了“别无限增长”,但它的代价是早期信息直接消失。Compaction 的思路是:到一定阈值就调一次 LLM,把早期对话压成摘要,保留近 N 轮原文。
@Bean
public CompactionConfig compactionConfig() {
return CompactionConfig.builder()
.triggerMessages(50) // 50 条消息触发压缩
.triggerTokens(0) // 不按 token 触发
.keepMessages(20) // 压缩后保留最近 20 条原文
.build();
}
这里采用按消息数触发,而不是按 Token 数触发。原因是机器人领域的工具结果长度波动很大:有时一句问答,有时返回一批产品数据。先按消息数把行为稳定下来,后续积累真实 Token 数据后,再调整成更精细的预算策略。
3. 双层长期记忆:MemoryConfig
这是阶段二我最喜欢的一个能力。先解释"双层"是啥意思:
- 第一层:每日流水账
workspace/memory/2026-07-19.md。每轮对话结束后,框架调一次 LLM 判断"这轮有什么值得长期记的",把事实追加到当日文件。 - 第二层:长期记忆
workspace/MEMORY.md。后台周期性任务读取多日流水账,再调一次 LLM 做合并、去重、提炼,生成结构化的"用户画像 + 长期约束"。
每轮 Agent 推理启动时,MEMORY.md 顶部几行会自动注入 system prompt。所以用户上周说过"预算 50 万",这周开新会话 Agent 直接就知道了。

@Bean
public MemoryConfig memoryConfig(OpenAIChatModel chatModel) {
return MemoryConfig.builder()
.model(chatModel)
.consolidationMinGap(Duration.ofMinutes(5)) // 合并最小间隔 5 分钟
.consolidationMaxTokens(2048) // 合并最多消耗 2048 tokens
.flushTrigger(MemoryConfig.FlushTrigger.throttled(Duration.ofMinutes(5)))
.build();
}
这里最关键的是 throttled(5 分钟)。一开始如果每轮对话都提炼一次记忆,模型调用次数和 Token 会明显增加,而且大量无价值信息会污染记忆。节流后,框架会在合适的间隔里把有价值的事实写到按日期的记忆文件,并逐步汇总到 MEMORY.md。
我的体会是:长期记忆不是聊天记录备份。它更像用户档案,只应该沉淀稳定、可复用、对后续决策有帮助的信息。
4. Channel 路由:HarnessGateway
阶段一是直接 chatClient.prompt().call(),阶段二改成了 HarnessGateway。Gateway 的核心能力是按 Channel 维度做路由和串行调度:
@Bean
public HarnessGateway harnessGateway(HarnessAgent harnessAgent) {
HarnessGateway gateway = HarnessGateway.create();
gateway.bindMainAgent(harnessAgent);
return gateway;
}
SessionService 里把 sessionId 映射为 Channel ID:
public String chat(String sessionId, String message) {
ensureSessionExists(sessionId);
String userId = resolveUserId(null);
MsgContext msgContext = buildMsgContext(sessionId, userId);
OutboundAddress outbound = OutboundAddress.direct(sessionId, userId);
Msg userMessage = new UserMessage(message);
// 通过 Gateway 路由到主 Agent
Msg response = harnessGateway.run(msgContext, List.of(userMessage), outbound).block();
return response != null ? response.getTextContent() : "";
}
private MsgContext buildMsgContext(String sessionId, String userId) {
return new MsgContext(
sessionId, // channel: 每个 session 对应一个独立 Channel
null, null, null, null,
Map.of("userId", userId, "source", "lingnova-web"),
userId
);
}
提问:这里为什么不用阶段一的 RuntimeContext,而要用 MsgContext?
答:RuntimeContext 是"Agent 内部"的上下文(告诉单个 Agent 当前 userId/sessionId 是什么),而 MsgContext 是"Gateway 路由层"的上下文(多了一个 Channel 维度,用来做并发隔离和路由)。改用 Gateway 之后,同一会话的并发请求会被串行调度,避免多个请求同时写坏 AgentState。这个在阶段一是完全没有的保障。
怎么理解路由和串行调度?
路由:假设gateway中注册了多个Agent,那么gateway就排上用场了,但是在目前的项目中,使用的是Supervisor模式,也就是一个主Agent管理多个子Agent,gateway只会路由到这个主Agent,然后由主Agent分配任务到子Agent,所以当前是没用到路由功能的。
串行调度:可以理解为是个按照channel串行调度的队列,如果用户在同一个 session 里快速发两条消息,第二条会被排队,等第一条执行完才进入 Agent。阶段一中是这么写智能体调用的:
// 假设这么写(阶段一风格)
harnessAgent.call(messages, runtimeContext).block();
这其实存在一个问题:Agent 本身在 2.0 是 无状态且线程安全 的,但 AgentStateStore (Redis)的读写没有锁——两个并发请求可能同时读到旧状态、同时写回,互相覆盖。所以Gateway 的 Channel 机制就是用来防止这个的。
5. 子 Agent 编排:Supervisor 多 Agent 架构
阶段一的 Agent 是"全能选手",什么都自己干。阶段二引入了三个专业子 Agent:
product-advisor:产品选型顾问,负责"推荐一款焊接机器人"这类需求troubleshooter:故障排查专家,负责"机器人关节过热报警怎么办"news-aggregator:资讯聚合助手,负责"本周 AI 行业动态整理"
主 Agent 做 Supervisor,根据用户意图自动委派任务:
private SubagentDeclaration productAdvisorDeclaration(String modelName) {
return SubagentDeclaration.builder()
.name("product-advisor")
.description("机器人产品选型顾问,擅长根据预算、场景、品牌做产品推荐和对比分析。")
.model(modelName)
.maxIters(10)
.workspaceMode(WorkspaceMode.SHARED) // 共享 workspace,读取共同记忆
.tools(List.of("searchRobots", "queryRobots", "getRobotDetail",
"getRecommendedRobots", "queryKnowledge"))
.build();
}
子 Agent 的关键设计是 WorkspaceMode.SHARED——和主 Agent 共享同一个 workspace,可以直接读取 MEMORY.md 和知识文件,不用重复加载。每个子 Agent 只暴露它需要的工具,避免越权调用。
maxIters 是一个很实际的保护:不让子 Agent 无限思考或反复调用工具。
实际效果是:用户问"推荐预算 50 万的焊接机器人",主 Agent 判断意图后把任务交给 product-advisor,它用产品查询工具拉数据、做对比、返回结构化建议,主 Agent 再整合后回复用户。这比一个 Agent 从头干到尾要稳定得多。
6. 技能装配:FileSystemSkillRepository
这个能力解决的是"运营改业务流程不用动代码"的问题。技能就是一份 Markdown 写的"操作手册",放在 workspace/skills/<skill-name>/SKILL.md:
@Bean
public AgentSkillRepository fileSystemSkillRepository() throws IOException {
Path skillsPath = Paths.get(WORKSPACE_ROOT).resolve("skills");
Files.createDirectories(skillsPath);
createSampleSkillIfAbsent(skillsPath); // 生成示例技能,方便运营参考
return new FileSystemSkillRepository(skillsPath, true);
}
示例技能 robot-selection-report,告诉运营人员 SKILL.md 长什么样:
---
name: robot-selection-report
description: |
生成机器人产品选型对比报告。
当用户需要"帮我写一份焊接机器人选型报告"时调用。
allowed-tools: [searchRobots, queryRobots, getRobotDetail, getRecommendedRobots]
---
# 机器人选型报告生成技能
## 执行步骤
1. 用 searchRobots 或 queryRobots 检索候选产品
2. 对重点产品用 getRobotDetail 获取详细参数
3. 从负载、臂展、精度、价格、品牌、适用场景六个维度做对比
4. 输出报告结构:背景 → 需求 → 候选产品 → 对比表 → 推荐结论 → 风险提示
5. 将报告保存到 workspace/reports/robot-selection-report.md
技能的工作机制叫"渐进式披露"——Agent 启动时只加载技能的 name 和 description(占用很少的 token),运行时判断需要某个技能,才调用内置工具 load_skill_through_path 把完整指令读进上下文。这个设计很巧妙,可以挂几百个技能而不撑爆 Prompt。
7. 工具白名单 + MCP 集成:ToolsConfig
阶段一的工具管理是"注册了就能用",阶段二加了白名单和黑名单:
@Bean
public ToolsConfig toolsConfig() {
ToolsConfig toolsConfig = new ToolsConfig();
// 白名单:只有列表中的工具才会暴露给 Agent
toolsConfig.setAllow(List.of(
"searchRobots", "queryRobots", "getRobotDetail",
"getRecommendedRobots", "queryKnowledge",
"searchArticles", "getArticleDetail",
"load_skill_through_path", "reset_tools"
));
// 黑名单:明确禁止的高危工具
toolsConfig.setDeny(List.of("shell_exec", "run_shell_command", "execute_shell"));
// MCP Server 示例:计算器,处理报价、ROI 等数值任务
McpServerConfig calculatorMcp = new McpServerConfig();
calculatorMcp.setTransport("stdio");
calculatorMcp.setCommand("npx");
calculatorMcp.setArgs(List.of("-y", "@modelcontextprotocol/server-calculator"));
calculatorMcp.setEnableTools(List.of("calculate", "add", "subtract", "multiply", "divide"));
calculatorMcp.setTimeout(Duration.ofSeconds(30));
toolsConfig.setMcpServers(Map.of("calculator", calculatorMcp));
return toolsConfig;
}
MCP(Model Context Protocol)是 Anthropic 提的工具协议,AgentScope 2.0 原生支持。好处是接入外部工具不用写 Java 代码,配置一段就行。比如以后要接 GitHub、PostgreSQL、Slack 的 MCP Server,都是在 toolsConfig.setMcpServers(...) 里加一段配置。
8. 计划模式 + 大工具结果卸载
这两个能力改动很小但效果立竿见影。
计划模式:复杂任务强制 Agent 先写计划再执行。开启方式就是两行:
.planModeEnabled(true)
.planModeAllowShell(false) // 计划阶段不允许执行 shell,避免误操作
.planFileDir(workspacePath.resolve("plans").toString())
开启后 Agent 会有一套内置工具(plan_enter / plan_write / plan_exit / todo_write),在 plan 模式下必须先把任务拆解成计划文件,再退出 plan 模式按计划执行。对于"帮我调研 5 款焊接机器人并写选型报告"这种长任务特别有用。
大工具结果卸载:工具返回结果太长(比如一次返回 100 条机器人详情)会撑爆 token。这个配置让超长结果自动落盘到 workspace/tool_results/,上下文里只保留前 1000 字符摘要:
@Bean
public ToolResultEvictionConfig toolResultEvictionConfig() {
return ToolResultEvictionConfig.builder()
.maxResultChars(8000) // 超过 8k 字符触发卸载
.previewChars(1000) // 保留前 1000 字符作为摘要
.evictionPath("tool_results")
.excludedToolNames(Set.of("getRobotDetail", "getRecommendedRobots", "getArticleDetail"))
.build();
}
excludedToolNames 是排除列表,结果通常较短的工具(单条详情)不走卸载,避免无谓的文件 IO。
9.工作区与文件隔离:Workspace
Workspace 是 Harness 里我很喜欢的设计。它不是一个抽象概念,而是真正的目录:知识、技能、记忆、计划和超长工具结果都能落在里面,看得见摸得着,方便你对Agent更好的控制。
workspace/
├── AGENTS.md # Agent 的角色和行为规则
├── MEMORY.md # 汇总后的长期记忆
├── knowledge/ # 领域知识文件
├── skills/ # 可复用的 SKILL.md
├── plans/ # 复杂任务的计划文件
├── memory/ # 按日期沉淀的记忆
└── tool_results/ # 被卸载的大工具结果
开发环境使用以 workspace 为根目录的本地文件系统,并把隔离范围设为 Session:
@Bean
public LocalFilesystemSpec localFilesystemSpec() {
return new LocalFilesystemSpec()
.project(Paths.get("./workspace"))
.mode(LocalFsMode.ROOTED)
.projectWritable(true)
.isolationScope(IsolationScope.SESSION);
}
ROOTED 的意思很朴素:Agent 只能把 workspace 当作自己的根目录使用,而不是随意读写宿主机。生产环境则可以通过 Profile 切到 Docker 文件系统,限制内存、CPU,并默认关闭网络:
@Bean
@Profile("prod")
public DockerFilesystemSpec dockerFilesystemSpec() {
return new DockerFilesystemSpec()
.image("python:3.11-slim")
.workspaceRoot("./workspace")
.memorySizeBytes(512 * 1024 * 1024L)
.cpuCount(1L)
.network("none");
}
目前 LingNova 的业务 Tool 主要是 REST 查询,不需要执行 shell;Docker 沙箱是为后续生成报告、处理文件、运行受控脚本预留的安全边界,不能把它误解为已经允许 Agent 随意执行命令。
六、把这些拼起来:最终的 HarnessAgent 配置
所有 Bean 准备好之后,最终的 HarnessAgent 配置其实非常简洁:
@Bean(AGENT_NAME)
public HarnessAgent harnessAgent(
AgentStateStore agentStateStore,
CompactionConfig compactionConfig,
MemoryConfig memoryConfig,
ToolResultEvictionConfig toolResultEvictionConfig,
ToolsConfig toolsConfig,
AgentSkillRepository fileSystemSkillRepository,
LocalFilesystemSpec localFilesystemSpec,
ObjectProvider<DockerFilesystemSpec> dockerFilesystemSpecProvider,
OpenAIChatModel chatModel,
AiContentProperties properties) {
Path workspacePath = Paths.get(WORKSPACE_ROOT);
initializeWorkspace(workspacePath);
String modelName = properties.getModelConfig().getModel();
HarnessAgent.Builder builder = HarnessAgent.builder()
.name(AGENT_NAME)
.description("灵核新纪 LingNova 机器人行业智能管家 - Supervisor Agent")
.sysPrompt(SYSTEM_PROMPT)
.model(chatModel)
.workspace(workspacePath)
.stateStore(agentStateStore)
.compaction(compactionConfig)
.memory(memoryConfig)
.toolResultEviction(toolResultEvictionConfig)
.toolsConfig(toolsConfig)
.skillRepository(fileSystemSkillRepository)
.planModeEnabled(true)
.planModeAllowShell(false)
.planFileDir(workspacePath.resolve("plans").toString())
.skillManageToolEnabled(true)
.subagent(productAdvisorDeclaration(modelName))
.subagent(troubleshooterDeclaration(modelName))
.subagent(newsAggregatorDeclaration(modelName));
// 根据配置选择文件系统实现
DockerFilesystemSpec dockerSpec = dockerFilesystemSpecProvider.getIfAvailable();
if ("docker".equalsIgnoreCase(filesystemMode) && dockerSpec != null) {
builder.filesystem(dockerSpec);
} else {
builder.filesystem(localFilesystemSpec);
}
return builder.build();
}
看这个 Builder 链,其实就是在回答几个问题:
- Agent 叫什么、人格是什么 →
name/description/sysPrompt - 用哪个模型 →
model - 工作目录在哪 →
workspace - 状态存哪 →
stateStore - 上下文怎么压缩 →
compaction - 长期记忆怎么管 →
memory - 长结果怎么处理 →
toolResultEviction - 能用哪些工具 →
toolsConfig - 业务知识从哪来 →
skillRepository - 复杂任务怎么规划 →
planModeEnabled - 子 Agent 怎么分工 →
subagent - 文件操作在哪跑 →
filesystem
每一行配置都对应一个具体的工程问题。这种"配置即架构"的写法,比阶段一那种"逻辑全写在 Service 里"要清晰太多了。
七、踩过的坑和几点感悟
1. 版本升级前先看 release notes
从 RC 版升到 2.0.0 GA,API 变了不少。最大的坑是模型提供商模块化——OpenAI、DashScope 等从 core 拆出去了,得单独加 agentscope-extensions-model-openai 依赖。我一开始按 RC 版的写法 import io.agentscope.core.model.OpenAIChatModel,编译怎么都过不去。后来看了 release notes 才知道要改成 io.agentscope.extensions.model.openai.OpenAIChatModel。
教训:升级大版本前,先扫一遍 release notes 的"重大变更"章节,能省掉一半调试时间。另外,新版本也会封装很多有用的接口,可以让代码更优雅。如果遇到大一点的版本更新,导入的包路径不对或不知道哪些新功能,可以让AI反向检索引进包的路径变化和新功能。
2. 包路径变更比 API 变更更隐蔽
CompactionConfig 在 RC 版的包路径是 io.agentscope.harness.compaction.CompactionConfig,GA 版挪到了 io.agentscope.harness.agent.memory.compaction.CompactionConfig。IDE 不会自动提示这种包路径变更,只能靠报错一个一个改。类似的还有 HarnessAgent 从 io.agentscope.harness.HarnessAgent 挪到了 io.agentscope.harness.agent.HarnessAgent。
3. @ToolParam 的 name 属性在 2.0 没有默认值了
阶段一的代码里 @ToolParam(description = "...") 能跑,升到 2.0 之后直接编译报错"name 属性必填"。这个变更其实挺合理——工具参数名应该显式声明,不能默认用反射拿到的参数名(不同编译器行为不一致)。但改起来要扫一遍所有 @ToolParam 注解,工作量不小。
4. Jedis 5.x 的 API 变化
前面提过,JedisPooled 的构造方式变了,不再支持无参构造后调 auth() / select()。这个坑其实跟 AgentScope 没关系,是 Jedis 自己升级带来的,但因为我们用了 RedisAgentStateStore,间接被影响。依赖树的传递变更真的防不胜防。
5. 不要一上来就把所有能力都开起来
我最初的想法是"既然框架提供了这么多能力,全开呗"。结果 Agent 启动慢了三倍,每轮对话 token 消耗翻倍,调试还特别困难——出问题了不知道是哪个组件导致的。
后来改成渐进式开启:先只开 stateStore + compaction,跑通;再加 memory,观察 MEMORY.md 是否正确生成;再加 subagent,测试委派逻辑;最后才加 planMode、toolResultEviction、skillRepository。每加一个能力就跑一遍验证,出问题能快速定位是新加的组件导致的。
6. Harness 不是银弹,它解决的是"工程化"问题
引入 HarnessAgent 之后,确实长会话稳定了、状态可恢复了、工具调用可控了。但它没有解决"模型回答质量"问题——模型还是会偶尔幻觉、还是会选错工具。Harness 做的是让这些错误不致命:选错工具可以被权限拦截、幻觉可以被记忆和知识库约束、长任务跑偏可以被 plan mode 约束。
所以我的理解是:Harness 把 Agent 的下限拉高了,但上限还是看模型 + 业务设计。这也是为什么阶段三还要继续做 RAG 和多 Agent 协作——前者提升知识准确性,后者提升任务拆解能力。
八、阶段二的验收
按开发计划里的验收标准过了一遍:
- 同一 sessionId 多轮对话能记住前文 ——
AgentStateStore自动恢复 - 服务重启后,同一 sessionId 能恢复对话 —— Redis 持久化
- 100 轮对话后 Token 使用量稳定 —— Compaction + 大结果卸载
- MEMORY.md 中能看到提取的用户偏好 —— 双层长期记忆生效
- 复杂任务先写计划再执行 —— Plan Mode
- 不同会话的文件互不干扰 ——
IsolationScope.SESSION - 子 Agent 能被正确委派 —— Supervisor 架构
还有几项要等阶段三才能完整验证:
- 工具调用失败自动重试
- 子 Agent 循环检测
九、下一步
阶段二把"工程化地基"铺好了,阶段三要做的是在这套地基上叠加"智能能力":
- 接入 PGVector 向量库,实现真正的 RAG(查询重写、混合检索、重排序、冲突检测)
- 把
lingnova-robot-service的 API 包装成 MCP Server,让工具生态更规范 - 工具调用的幂等、重试、熔断、超时——
Resilience4j该上了 - 子 Agent 的循环检测和强制终止
- 评估体系搭建,跑 50 条机器人领域评估集
到阶段三结束,LingNova 应该就能算一个"企业级可用"的 Agent 了。阶段四再补上权限隔离、安全护栏、可观测性等。
最后说一句:写这篇文章的时候我翻了翻阶段一的博客,当时结尾写的是"Agent 的难点不在于调用一次大模型,而在于让它在多轮、长任务、失败和重启之后依然行为稳定"。阶段二做完,这句话算是兑现了一半——稳定性的机制有了,剩下的就是让智能能力跟上来。
相关链接
更多推荐



所有评论(0)