AgentScope第4式·启灵入门
AgentScope第4式·最小启动
数字人已经有宿主了,但"大脑"还是空的,这一式只解决第一句话由谁来说。
[读者] 已经跑通数字人宿主、准备接推理内核的 Java 工程师
[痛点] 双层架构、v1/v2 边界、依赖坐标都不确定,不知道第一行代码写在哪
[现在读] 第3式已经冻结了唯一 Demo 基线,宿主齐了,只缺推理入口
[读完] 能用 HarnessAgent 跑通"一句话进、一句话出",并拿到 sessionId 与耗时
[旧方案] Bridge 收到消息先打日志,回一句写死的话术,推理层留空
|
v
[新需求] 数字人要真的会答话,而且这次调用必须可记录、可复查
|
v
[冲突] AgentScope Java 里 ReActAgent 和 HarnessAgent 都像入口,选错后面全返工
|
v
[后果] Memory、Tool、Plan 全部推倒重来,Demo 沦为一次性玩具
我是老李。上个月我们把最小交互数字人 Demo 冻结在 chapter/03-digital-human-demo:Spring Boot 加 MySQL 管项目配置,运行页 /run/{projectId} 负责展示并接入 LiveKit 房间,Realtime Bridge 只做媒体与消息适配。房间能连,音频能跑,浏览器里能看到自己的画面。
但那个页面是"哑"的。Bridge 收到事件以后只干两件事:打日志,回一句写死的兜底话术。真正的推理层是空的——从第3式结束那一刻起,它就是空的。
现在是第4式。任务是给这个已经冻结的宿主接上第一层推理能力:用户在命令行敲一句话,后端拿到模型生成的回复,并且把这次的模型、sessionId、耗时记录下来。不做口型,不做表情,不做视频合成,只做"会说第一句话"。
可这里有个绕不开的岔路:AgentScope Java 2.x 里,ReActAgent 和 HarnessAgent 都像是"入口"。凭直觉,ReActAgent 是推理,应该从它开始;但官方 quickstart 摆在那儿的却是 HarnessAgent。
那第一行代码到底写在哪一层?
我是该直接 new 一个 ReActAgent,还是从 HarnessAgent 进?
如果选错了,后面 Memory、Tool、Plan 全部要返工,你准备为一次"看起来更快"的选型付费吗?
01、故事
现场是本机三个进程:MySQL、Spring Boot、LiveKit Server。浏览器打开运行页 /run/{projectId},左上角是项目名,中间是视频窗格,底部有一个文本输入框。那个输入框在第3式里没有后端处理逻辑,它只是个装饰。
当前方案很直白:输入框把文本事件发给 Bridge,Bridge 打日志,回固定话术。团队里有人说过一句很典型的话——"先这样,能演示就行"。这话在演示当天是对的,在第4式就是错的。
变化发生在这一式:固定话术要被替换成模型回复。而且不是随便替换。这次调用必须记录模型名、sessionId、耗时。理由很实际:后面要做多轮对话、记忆、工具,没有这三样信息,你连"上一轮到底发生了什么"都查不了,只能靠猜。
冲突集中在选型上。AgentScope 生态里既有 Python 版,也有 Java 版;Java 版又有早期版本的老 API 和 2.x 主线;2.x 主线里 ReActAgent 和 HarnessAgent 看起来都在讲 Agent。四层歧义叠在一起,足够让一个只想跑通第一句话的人卡上一整天。
所以这一式我给自己定了条原则:先跑通最小闭环,再谈高级能力。而跑通的判据不是"能对话",是"能对话加能留痕"。
宿主虽已立
大脑犹未接
先求一句话
再论千般能
02、问题
旧认知是这样的:接大模型就是加一个 HTTP SDK,POST 一个 chat 接口,把返回的 content 打印出来。二十行代码,半小时搞定,成就感很强。
为什么不够?因为数字人不是一次性问答。它要记住这是第几轮、这个用户是谁、下一步要不要调工具。裸接 SDK 的话,每加一项能力你都要自己写一层胶水;写到第三项的时候,胶水本身就成了一个新的、没人维护的小框架。你并没有省掉抽象,只是把抽象写得更差。
证据是公开的:AgentScope Java 把 harness 单独做成一个模块,而不是塞进推理核心里。官方 Java 站(java.agentscope.io)和 v2.0.3 仓库文档区的 quickstart,直接指向 HarnessAgent,而不是让你先从 ReAct 循环手写起。这说明在这套设计里,"工程运行"是一等公民,不是附赠品。
风险也很清楚:如果你无视这层,直接从裸 SDK 起,第5式加 Tool 的时候你会发现没有地方挂;第6式加 Memory 的时候你会发现没有会话可挂;最后要么重写,要么在 Demo 上贴补丁,而补丁会一直跟着你。
本篇的完成标准是可验证的:命令行输入一句话,输出模型回复;同时打印本次调用的模型名、sessionId、耗时。三个都要,少一个不算完成。
裸调一时快
胶水自承担
先定可验证
再谈选哪层
03、原理
先给第一个反直觉判断:HarnessAgent 不是"更高级的 ReActAgent"。这两个东西根本不在同一个轴上。
ReActAgent 解决的是"一次推理怎么循环"——思考、行动、观察、再思考。HarnessAgent 解决的是"一个 Agent 怎么被运行和管理"——配置从哪来、会话怎么标识、扩展点挂在哪、运行产物怎么留。前者是内核,后者是工程层。把它们理解成"简单版和高级版",后面的架构你会全部看歪。
第二个反直觉判断:最小可用 Demo 不该从最底层 API 开始。很多人的直觉是"从底层学起才扎实",但在框架里跑第一句话,从底层起恰恰最慢——因为底层暴露的抽象变量最多,每个变量都是一个可以选错的地方。先用工程层的默认值跑通,再回头拆内核,顺序才对。
三个名词,各一句话说清边界,不做百科展开:
- ReActAgent:推理内核,负责"思考—行动—观察"的循环。
- HarnessAgent:工程层,把推理内核包装成可配置、可会话、可扩展的运行单元。
- Harness:不是套壳,是 Agent 的运行外壳与扩展点的宿主。
关于 v1 API 与 v2 主线的边界:v2.0.3 是本系列基线,这一式只走 v2 主线的 harness 入口。AgentScope Java 早期版本有另一套 API 心智模型,两者能否混用、兼容层保留到什么程度,属于需要按官方 Release Notes 与仓库目录核验的事实,本文不依赖它,也不把旧写法当成 v2 写法。当前最新正式版请以 releases 页面为准,本文实验按 v2.0.3 复现。
内核管循环
工程管运行
分层非洁癖
变更各安身
04、架构
[输入] 命令行一句话 / 后续由 Bridge 转发的文本事件
|
v
[模块] HarnessAgent 工程层:配置、会话、扩展点宿主
|
v
[状态] sessionId + 本次会话上下文
|
v
[处理] ReActAgent 推理内核 -> LLM
|
v
[输出] 文本回复 + model + sessionId + 耗时
边界:HarnessAgent 只负责"配置、会话、扩展点",真正调模型发生在 ReActAgent 的循环里;Realtime Bridge 依旧不碰推理,它只把媒体/消息事件送进来。这条边界不划清,后面 Bridge 会越来越胖,最后变成第二个业务后端。
收益:多一层抽象,换来的是扩展位。后面 Memory、Tool、Skill、Plan、Subagent 都能挂在这一层,不用回头改宿主。
代价:多一个依赖、多一次构造、多一层调试栈。出问题时堆栈更深,日志更多,定位路径更长。
适用条件:需要多轮会话、需要留痕、需要后续挂工具的项目。
不适用条件:一次性的无状态文本转换;对冷启动极其敏感的短生命周期任务;只想横向对比模型本身能力、不需要框架介入的纯 prompt 评测。
源码位置:v2.0.3 的 HarnessAgent 位于 agentscope-harness 模块下的 io/agentscope/harness/agent/HarnessAgent.java。读它的时候带着一个问题——它是怎么持有并调度 ReActAgent 的?这个答案决定了后面所有扩展点挂在哪。
课程代码仓库:https://github.com/lifuchun522/agentscopepractice
一句话进门
一行答出门
状态留在场
耗时留在册
05、实战一次
环境:JDK 17+、Maven 3.9+。先从第3式分支继续,不覆盖老分支。
git checkout chapter/03-digital-human-demo
git checkout -b chapter/04-bootstrap
依赖以官方 quickstart(v2.0.3 tag 下的 docs/v2/zh/docs/quickstart.md)为准,坐标为:
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-harness</artifactId>
<version>2.0.3</version>
</dependency>
最小配置抽成一个 record,顺便把"模型、提示词"从代码里挪出来:
package com.lifuchun.digitalhuman.bootstrap;
public record DigitalHumanAgentConfig(String agentName, String model, String systemPrompt) {
public static DigitalHumanAgentConfig fromEnv() {
String name = System.getenv().getOrDefault("DH_AGENT_NAME", "digital-human-demo");
String model = System.getenv("DH_MODEL");
String prompt = System.getenv().getOrDefault("DH_SYSTEM_PROMPT",
"你是一个数字人助手,用中文回答,控制在两句话以内。");
if (model == null || model.isBlank()) {
throw new IllegalStateException("缺少环境变量 DH_MODEL");
}
return new DigitalHumanAgentConfig(name, model, prompt);
}
}
主类跑通最小闭环,同时把三元信息打出来:
package com.lifuchun.digitalhuman.bootstrap;
import io.agentscope.harness.agent.HarnessAgent;
import java.util.Scanner;
public class BootstrapMain {
public static void main(String[] args) {
DigitalHumanAgentConfig cfg = DigitalHumanAgentConfig.fromEnv();
// 字段名与入参类型以 v2.0.3 的 HarnessAgent.java 与官方 quickstart 为准
HarnessAgent agent = HarnessAgent.builder()
.name(cfg.agentName())
.model(cfg.model())
.systemPrompt(cfg.systemPrompt())
.build();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("你:");
if (!scanner.hasNextLine()) {
break;
}
String input = scanner.nextLine().trim();
if (input.isEmpty()) {
continue;
}
if ("exit".equalsIgnoreCase(input)) {
break;
}
long start = System.currentTimeMillis();
String reply = agent.chat(input);
long costMs = System.currentTimeMillis() - start;
System.out.println("数字人:" + reply);
System.out.println("[meta] model=" + cfg.model()
+ " sessionId=" + sessionIdOf(agent)
+ " costMs=" + costMs);
}
}
// 会话标识的读取方式以 HarnessAgent 源码暴露的接口为准,此处仅为占位签名
private static String sessionIdOf(HarnessAgent agent) {
return String.valueOf(agent);
}
}
启动:
export DH_MODEL=<你在配置里指定的模型>
export DASHSCOPE_API_KEY=<你的密钥>
mvn -q compile
mvn -q exec:java -Dexec.mainClass=com.lifuchun.digitalhuman.bootstrap.BootstrapMain
说明:exec 插件需要在 pom 中声明;也可以直接在 IDE 里运行 BootstrapMain。以下为示例输出,未在当前环境实测,属于预期结果:
你:你好,介绍一下你自己
数字人:你好,我是这个项目的数字人助手,可以陪你熟悉项目配置和开场白。
[meta] model=<你在配置里指定的模型> sessionId=<本次会话标识> costMs=<本次耗时>
提交建议:
git add .
git commit -m "feat(ch04): bootstrap first AgentScope HarnessAgent"
验收清单只有三条:一句话进得去;一句话出得来;model、sessionId、耗时三项都留了档。
一分支起步
一依赖入门
一行问一句
一句答一声
06、排查
证据链一:依赖拉不下来。
现象:构建报 Could not resolve dependencies for project,指向 io.agentscope:agentscope-harness:2.0.3。
怀疑:镜像没配好;groupId 写错;版本号不存在;artifactId 记成了别的模块名。
检查:逐字符对照官方 quickstart 里的依赖片段;打开 releases 页确认 v2.0.3 这个 tag 确实存在。
证据:坐标以官方 quickstart 与 v2.0.3 标签为准,不以博客或记忆为准。
根因:坐标与官方不一致,最常见的两种是把 artifactId 写成整仓库名,或把版本写成带 SNAPSHOT 的号。
修复:改回官方坐标,重新拉取依赖。
错误尝试: 把 version 删掉,让 Maven 自动选最新版。为什么错:一旦 Maven 选到更新版本,你跑的就不再是 v2.0.3,本文后面所有结论对你都不可复现,而且"能不能用"会变成"看你今天几点构建"——排障变成抽签。
证据链二:以为必须先 new 一个 ReActAgent。
现象:照着 Python 版文档找 ReActAgent 的用法,到 Java 侧对不上,字段和方法名都不一致。
错误尝试: 把 AgentScope Python 的示例按行翻译成 Java。为什么错:AgentScope Python 与 AgentScope Java 是两套实现,能力不对等,类名相似不等于语义相同;把"Python 有"当成"Java 也有",是这类排查里最高频的误判。
正确做法:以 java.agentscope.io 和 v2.0.3 tag 下的 quickstart 为第一手依据。Java 侧的工程入口是 HarnessAgent,ReActAgent 是它下面的推理内核,不是起手式。
证据链三:启动即鉴权失败。
现象:进程起来后第一次对话超时或返回鉴权错误。
检查:环境变量是否真的进了当前进程。IDE 的运行配置通常不读 shell 的 profile,你在终端 export 的东西它看不见。
根因:密钥只在终端里 export 过,IDE 里跑的时候没带上;或者模型名与端点不匹配。
修复:在运行配置里显式设置环境变量,或统一用启动脚本注入。
上游贡献判断:本章未发现明确的上游缺陷。唯一可记录的是"quickstart 对 Maven 坐标与最小可运行样例的说明密度"这类文档候选——需要先对比 main 与 v2.0.3 的文档差异再决定是否进入 Issue,目前只能算候选,不构成 PR。
坐标错一字
排查白半天
基线不可漂
版本不可猜
07、优化
第06章给出的根因有三条,对应三处修改。
根因一:密钥与模型配置写在代码里或者散在终端里。修改:全部走环境变量,配置对象集中构造。原因:环境相关的东西不该进版本库,也不该靠记忆。新行为:换机器只要设三个变量。验证:清一次终端环境后重新启动,缺变量时应当立刻抛出明确异常,而不是等到第一次对话才失败。
根因二:元信息只在控制台打印,没人能回查。修改:把这一轮调用落成结构化记录。原因:第6式以后要按会话回放,字符串日志不够用。
package com.lifuchun.digitalhuman.bootstrap;
public record TurnRecord(String model, String sessionId, long costMs, String input, String reply) {
}
根因三:Agent 构造逻辑塞在主类里。修改:抽成工厂。原因:下一式挂 Memory 或 Tool 时,你只需要改工厂一处,不用动命令行循环。
package com.lifuchun.digitalhuman.bootstrap;
import io.agentscope.harness.agent.HarnessAgent;
public final class DigitalHumanAgentFactory {
private DigitalHumanAgentFactory() {
}
public static HarnessAgent create(DigitalHumanAgentConfig cfg) {
return HarnessAgent.builder()
.name(cfg.agentName())
.model(cfg.model())
.systemPrompt(cfg.systemPrompt())
.build();
}
}
新行为需要验证两点:连续三轮输入,sessionId 在同一会话内应保持稳定(具体语义以源码与官方文档为准);耗时应随回复长度变化,而不是恒定值——恒定的耗时通常说明你测的是本地缓存或桩实现。
未在当前环境实测的部分,均只作为验证方案,不作为已验证结论。
根因在环境
改动在封装
行为可复查
结论可复现
08、演进
[同一输入] 命令行一句话
|
+--[V1] 固定话术 / 裸接 SDK:无会话标识、无元信息
| 代价:不可扩展、不可复查、后续能力全部返工
|
+--[V2] HarnessAgent + 环境变量 + 元信息记录
| 代价:多一个依赖、多一层抽象、堆栈更深
|
[Trade-off]
得到:一个能挂工具的工程入口,和可追溯的会话轨迹
失去:一点直接控制的自由度,以及一点点启动开销
边界:单次无状态调用、极致冷启动场景,可以先不上
比较维度:
- 正确性:V1 的固定话术在"能答话"这条上根本不合格;V2 满足。
- 稳定性:V1 无外部依赖,看起来稳;V2 引入模型可用性变量,但换来可观测。
- 复杂度:V2 明显更高,多一个模块、多一层构造。
- 成本:V2 每次对话产生模型调用成本,V1 是零成本但零价值。
- 适用范围:V1 只适合演示;V2 才适合继续长。
- 遗留问题:V2 的会话还没有落库,Bridge 还没接上这条链路,多轮上下文仍是单进程内存态。
旧路走捷径
新路走工程
得在可扩展
失在多一层
09、洞见
9.1 双层架构不是分层洁癖,是把变更成本分开
如果把推理循环和运行管理揉在一层,任何一处改动都会牵动另一处。分开之后,加工具只动工程层,换推理策略只动内核层。分层的价值从来不是图好看,是让改动有边界。
9.2 反直觉判断:最小 Demo 应该从工程层进,而不是从内核进
直觉认为"从底层学起才扎实",但在框架里跑通第一步,抽象变量越少越好。工程层的默认值替你先做了很多决定,等链路通了再逐个替换成显式配置,认知成本反而更低。
9.3 反直觉判断:跑通不等于理解,但顺序不能反
先跑通再理解,和先理解再跑通,看起来只是顺序差别,实际是失败成本差别。跑通之后再读 HarnessAgent.java,你脑子里有可运行的参照物;反过来,你在还没见过一次成功调用的前提下读调用链,很容易把设计当成冗余。
9.4 什么时候不应该用本章这套
单次无状态文本转换、纯 prompt 横向评测、对冷启动极端敏感的一次性任务,都不需要引入工程层。这时候挂上 HarnessAgent,你多买的是会话与扩展位,而这两样你都不会用。判断标准很简单:未来三个月内,这个场景会不会出现第二次带上下文的调用?答不会,就别上。
入门走高层
深挖回内核
顺序若颠倒
返工在后面
10、系统落地
原来有什么:一套可运行的数字人宿主——项目管理 CRUD、名称/标题/主题色/背景图/开场白/结束语配置、运行页 /run/{projectId}、LiveKit 房间与最薄的 Realtime Bridge。推理层是空的。
本篇新增什么:一个可对话的最小后端。具体是三件事——引入 agentscope-harness 依赖,跑通一个最小 HarnessAgent,把每次调用的模型、sessionId、耗时留档。
现在能做什么:命令行一句话进、一句话出,并且这次调用可回查。数字人从"会放声音"变成"会答话",虽然还只在终端里。
还缺什么:会话没进库;工具没挂;Bridge 还没接上这个后端;运行页还没真正走这条链路;多轮上下文还在单进程内存里。
下一步如何演进:把工厂接进 Bridge,让运行页的输入框真正打到 Agent;再把会话与会话记录落进 MySQL,为后面的记忆与工具留位置。
开源参与点:本章未发现明确上游问题。若后续确认 quickstart 的依赖坐标说明与仓库实际发布状态不一致,需要先比对 v2.0.3 与 main 两份文档,确认不是已修复的旧版本问题,再考虑是否进入 Issue。
宿主已具备
推理才接入
会话待入库
工具待后章
11、小结
Q1 -> 第一行代码写在哪一层
答:从工程层 HarnessAgent 进,ReActAgent 是它下面的推理内核
Q2 -> 内核与工程层怎么分工
答:内核管"思考—行动—观察"循环,工程层管配置、会话与扩展点
Q3 -> 这一式怎么算完成
答:一句话进、一句话出,且模型、sessionId、耗时三项留档
状态 -> 数字人项目拥有"可对话最小后端",分支 chapter/04-bootstrap 可继续演进
一问定入口
二问定边界
三问定判据
状态可交付
12、作业
12.1 理解题:ReActAgent 与 HarnessAgent 的职责边界在哪里?
参考答案:ReActAgent 是推理内核,负责一次推理内部的循环;HarnessAgent 是工程层,负责把内核包装成可配置、可会话、可扩展的运行单元。判据是:凡是"改一次配置、开一次会话、挂一个扩展点"的需求,都属于工程层;凡是"这一轮该不该再调一次工具"的决策,都属于内核层。
12.2 实战题:把本章的模型、sessionId、耗时落成一行结构化日志。
参考答案:定义一个 record 承载四个字段(模型、会话标识、耗时、输入摘要),在命令行循环里组装并输出为单行 JSON。注意不要在日志里写入密钥;sessionId 的读取方式必须以 v2.0.3 源码暴露的接口为准,不能靠字符串拼接猜测。
12.3 排障题:构建报 Could not resolve dependencies 指向 agentscope-harness,列出至少三条排查路径。
参考答案:一,逐字符核对官方 quickstart 的坐标;二,打开 releases 页确认所用版本标签真实存在;三,检查本地镜像与网络;四,检查 pom 是否继承了会覆盖版本的父 pom。注意不要用"删除 version 让 Maven 自动选最新"来绕过——那会让基线漂移。
12.4 架构判断题:一个只做单次无状态文本翻译的服务,该不该上 HarnessAgent?
参考答案:默认不该。翻译是单次无状态调用,不需要会话标识,也不需要扩展位,引入工程层只会增加依赖与调试栈。请打开 v2.0.3 tag 下的 HarnessAgent.java 与其所在模块的官方文档,确认它提供的会话与扩展能力对这个场景是否属于净负担,再给出结论——不要只背本文的判断。
读码方知界
动手才见真
题在源码里
答在编译后
13、思考
这一式的核心冲突不是"会不会调模型",而是"第一行代码写在哪一层"。它之所以值得单开一篇,是因为这是个一次性的选择:选择工程层,你得到的是可扩展的运行外壳;选择裸接,你得到的是今天下午的成就感,和三个月后的一次重写。
把这个问题抽象出去,就得到一个可复用的判断:当一个框架同时提供内核 API 和工程 API 时,第一版永远从工程 API 进,理解永远回内核 API 补。顺序反了不是学得慢,是学得贵。
第4式之后,数字人终于会说话了,但它还只记得这一句话。会话怎么活过多轮、记忆挂在哪,是下一段路要处理的事。
会用框架,是知道 API 怎么调;能参与框架演进,是知道这个 API 为什么被放在这一层。差别就在这里。
先跑通再懂
先懂再扩展
会用是起点
能改才是本事

浙公网安备 33010602011771号