【阶段二】基于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、为什么选它

  1. 它是阿里出品,对Java友好,国内文档、issue 响应速度有保障,我这种中文母语开发者看起来不费劲。
  2. 它明确把自己定位成"Harness 框架",不是"又一个 Agent SDK"。它的核心抽象 HarnessAgent 就是为长跑、状态恢复、工程化部署设计的。
  3. 引入方便,只需要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 ChatClientAgentScope HarnessAgent
会话状态Redis 存 20 条消息(手写)AgentStateStore + RuntimeContext(框架托管)
多轮上下文窗口截断,超了就丢Compaction 压缩 + 大结果卸载
长期记忆双层 MEMORY.md + 每日流水账
子 AgentSupervisor 模式 + 3 个专业子 Agent
路由直接调 ChatClientHarnessGateway 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 不会自动提示这种包路径变更,只能靠报错一个一个改。类似的还有 HarnessAgentio.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,测试委派逻辑;最后才加 planModetoolResultEvictionskillRepository。每加一个能力就跑一遍验证,出问题能快速定位是新加的组件导致的。

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 的难点不在于调用一次大模型,而在于让它在多轮、长任务、失败和重启之后依然行为稳定"。阶段二做完,这句话算是兑现了一半——稳定性的机制有了,剩下的就是让智能能力跟上来。


相关链接

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐