AgentScope第8式·溯源源码
AgentScope第8式·源码地图
黑盒能跑通,却改不动——第一次把 AgentScope Java 拆开看。
[读者] 已跑通最小 Agent、会切模型、看过 AgentEvent 事件流的 Java 工程师
[痛点] 框架只能当黑盒用:行为不符时不知去哪找,想扩展时不知代码写哪一层
[现在读] 第8式要把源码研究方法定下来,后续每章涉及框架行为都要能落到官方源码与测试
[读完] 能从一次数字人请求出发,在 IDE 里跟到 ReActAgent、Model、Toolkit 与事件输出
[旧方案] 读官方文档 + 抄 examples,按类名和包名推断行为
|
v
[新需求] 要改推理流程、插 Middleware、接自定义 Tool、判断升级会不会踩到扩展点
|
v
[冲突] 文档描述的是设计意图,源码决定的是运行时行为,两者在边界处经常对不上
|
v
[后果] 改动靠试错,回归靠运气,升级靠祈祷
我是老李,做 Java 后端和 AI Agent 落地。到第7式为止,数字人项目已经能跑起来了:Web 管理端建项目,/run/{projectId} 打开运行页,LiveKit 建房间,Realtime Bridge 把实时会话接进 AgentScope Java,模型能回话,事件能推到前端。模型切换、ReAct 多轮、事件增量这三件事都验证过。
但从第9式开始,性质变了。要加一个自定义 Tool 让数字人查项目配置变更记录,要在推理链路上插一层业务校验,还要判断下一次框架升级会不会把扩展点挪走。这三件事,一件都靠读文档做不出来。文档会告诉你“框架支持扩展”,不会告诉你“在哪一层扩展、扩展点被谁调用、一轮推理里被调用几次”。
于是团队定了一条规矩:从这一式开始,任何涉及框架行为的结论,都必须能指出对应的官方源码位置或测试位置。指不出来的,先不要写进设计文档。
一个问题:一个类名,到底能证明什么?
一个选择:文档和源码打架的时候,你先信哪一个?
一个后果:如果连“这次升级会不会动到我的扩展点”都答不上来,你还敢不敢升级?
01、故事
周会白板上画着数字人的链路:Web 运行页 ⇄ LiveKit ⇄ Realtime Bridge ⇄ AgentScope Java ⇄ LLM。这条线在第7式已经跑通,谁来看都能看懂。
真正卡住的是下一句话。产品说,数字人得知道“这个项目最近改了哪些配置”,要接一个查询工具。任务是:在下一次迭代里,让数字人能调用一个自定义 Tool 去查项目配置变更记录。
我问了一个很简单的问题:这个 Tool,代码写在哪个模块?
会议室安静了几秒。有人说写在 Spring Boot 的 service 层,模型要查的时候从那边拿;有人说看官方 examples 里是注册到某个 Toolkit;还有人说“先按文档写,跑不通再调”。
三种答案,三种架构。选错一次,后面几章的代码全都要搬家。
于是我提了个更前置的要求:先把 AgentScope Java 的源码看一遍,把模块边界和调用链画出来,再决定 Tool 写在哪、中间件插在哪、事件从哪出来。这一式不写业务功能,只做一件事——给数字人项目建一张源码地图。
黑盒能跑难能改
文档能读难能证
一问落到哪一层
先去源码找答案
02、问题
过去半年的做法是:读官网文档 → 抄 agentscope-examples → 遇到不懂的类名就按名字猜。这套做法在前7式够用,因为前7式的目标只是“跑通”。
为什么不够?三个具体原因。
第一,文档写的是能力清单,不是调用顺序。它告诉你 ReAct、Tool、Middleware 都存在,不告诉你它们在一次请求里谁先谁后、谁调谁。
第二,examples 演示的是“怎么调通”,不是“哪一层负责什么”。examples 是最短路径,最短路径会跳过所有分支,也跳过所有边界。
第三,类名是命名约定,不是行为契约。HarnessAgent 听起来像“Agent 的外壳”,ReActAgent 听起来像“ReAct 循环本人”,但名字不告诉你依赖方向、不告诉你谁被谁持有、不告诉你哪一层是你可以安全改的。
业务上的表现很直接。想加一个 Tool,先要回答“Tool 注册在哪、被谁调度、调度结果怎么回到模型”;想加一层业务校验,先要回答“推理链路上有没有稳定的切点”。答不上来,就只能改一行试一试,试完再改一行。
技术上的表现是:改动没有边界,回归没有依据,升级没有判断。
本篇的完成标准有三条:一,能画出 agentscope-core、agentscope-harness、agentscope-extensions、agentscope-service、agentscope-examples、docs 的职责边界;二,能从一次数字人请求出发,在 IDE 里沿调用链跟到 ReActAgent、Model、Toolkit 和事件输出;三,能把结论写进 docs/source-map.md,并在教程仓库 chapter/08-source-code-map 分支上留下一个可提交的版本。
旧法能解一时急
新需求要改行为
猜出来的调用链
撑不起一次升级
03、原理
先给一个反直觉判断:模块划分的价值不在目录好看,而在依赖方向。看懂依赖方向,你才知道自己的代码应该放在哪一侧;看不懂依赖方向,你会在最上层写业务逻辑,然后在下一次升级里全部重写。
先把本篇要用的几个名词各用一句话划边界,不做百科展开:
- HarnessAgent:装配层与运行时外壳,把配置、模型、工具、会话组织成一次可运行的 Agent 会话。源码入口在
agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java。 - ReActAgent:能力内核,负责“想—做—再看”的推理轮次控制。源码入口在
agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java。 - Middleware:在推理链路的固定切点上插入横切逻辑的钩子,是“不改内核也能改行为”的入口。
- Model:模型适配层,把内核的推理请求翻译成具体 provider 的调用,屏蔽厂商差异。
- Toolkit / Tool:工具注册与调度,把模型吐出来的工具调用请求变成真实的方法调用,再把结果送回推理链。
- AgentEvent:内核对外的统一事件通道,文本增量、工具调用、工具结果等中间态都从这里流出去,Realtime Bridge 和 Web 运行页消费的就是它。
再看依赖方向。下面这张表是本章要核验的地图,必须以你本地克隆的 pom.xml 和 mvn -pl <module> dependency:tree 为准,不能照抄:
| 模块 | 定位 | 依赖侧 | 你大概率在哪写代码 |
|---|---|---|---|
| agentscope-core | 能力内核 | 被依赖 | 自定义 Tool、Middleware、事件订阅 |
| agentscope-harness | 装配与运行时外壳 | 依赖 core | 会话装配、运行配置 |
| agentscope-extensions | 可选集成 | 依赖 core | 接第三方模型、存储、协议 |
| agentscope-service | 服务化与部署侧 | 依赖 core / harness | 部署形态、服务封装 |
| agentscope-examples | 用法验证 | 依赖全部 | 抄用法、交叉验证调用关系 |
| docs | 意图说明 | 无 | 先读,再去源码核对 |
第二个反直觉判断:越靠近上层的类,越不该是你改的地方。HarnessAgent 在 harness 模块,是装配层;ReActAgent 在 core 模块,是内核。如果你的业务逻辑写在装配层,就等于把业务绑在“怎么组装”上,而组装方式是最容易随版本变化的部分。真正稳定的扩展点是内核提供的抽象:Tool、Middleware、事件。
这条判断会直接决定后面的架构、排查和优化:扩展点写在内核抽象上,升级风险最小;写在装配层上,升级风险最大。
目录不是为好看
依赖方向才是界
类名只作路标用
行为要看调用链
04、架构
[输入] /run/{projectId} 的会话启动请求
|
v
[模块] HarnessAgent(装配)→ ReActAgent(内核循环)
|
v
[数据/状态] 会话消息 / 推理请求 / 工具调用请求
|
v
[处理] Middleware 切点 → Model 推理 → Toolkit 调度 Tool
|
v
[输出] AgentEvent 增量流 → Realtime Bridge → LiveKit → Web 运行页
边界:LiveKit 只管 WebRTC 房间与实时音频/数据传输;AgentScope Java 管推理、Tool、状态、Harness、后续的 Memory 与 Subagent;Realtime Bridge 只做媒体与消息适配,不承载业务规则。
收益:这条链把“实时传输”和“智能推理”彻底分开。传输层抖动不影响推理逻辑,推理逻辑升级也不需要动传输层。对本章来说,更重要的是:链路上每一段都对应一个明确的模块,可以在 IDE 里逐段下断点。
代价:跨进程/跨模块的链路让“跟着一次请求走完”变难,需要源码级调试能力。多一层 Bridge,就多一层需要被观测的地方。
适用条件:需要把实时交互和 Agent 推理解耦的项目;需要给数字人接自定义工具、插业务校验的项目。
不适用条件:只做一次性脚本验证、不打算长期演进、也不需要改框架行为时,不需要做源码走读,直接按 examples 跑通即可。第3式已经明确过范围——不做口型、表情、动作、复杂视频合成,所以本式的走读范围也只落在推理链上,不涉及任何媒体合成逻辑。
官方源码位置:本章涉及的框架入口,以固定 tag 为准。核心模块目录见 agentscope-core(v2.0.3),装配层目录见 agentscope-harness(v2.0.3),用法验证见 agentscope-examples(v2.0.3)。仓库总入口是 agentscope-java。
需要提醒版本口径:本章所有源码结论以 v2.0.3 tag 为准,main 分支只用来判断“某个问题是否已经被修复”。写作时请打开 Releases 核对当时的最新正式版;如果最新版已不是 v2.0.3,本文的走读步骤仍可在 v2.0.3 上复现,但不要把 v2.0.3 的限制当成当前最新版的永久限制。
输入一路向下走
模块各守一段边
状态落在内核里
事件流出到前端
05、实战一次
本章的“实战”不是写业务功能,而是完成一次可复现的源码走读,并留下产出物。产出物有两个:docs/source-map.md 和 scripts/source-map-check.sh。
第一步,定基线。 不要在主分支上研究,先把 tag 切出来:
git clone https://github.com/agentscope-ai/agentscope-java.git
cd agentscope-java
git fetch --tags --all
git checkout v2.0.3
git rev-parse --short HEAD
第二步,绑定源码。 有两种方式,按依赖方向选:
方式 A(推荐,最稳):把 agentscope-java 作为多模块工程直接导入 IDE,所有模块都是源码,不存在反编译。
方式 B:数字人 Demo 通过 Maven 依赖使用框架时,在工程根目录执行 mvn dependency:sources 拉取 sources 包,再在 IDE 里绑定 Sources。若该命令没有拉到源码,不要立刻判定“官方没开源”——先去 tag 页面看 src/main/java 是否完整,再决定改用方式 A。
第三步,写模块自检脚本。 在教程仓库新建 scripts/source-map-check.sh:
#!/usr/bin/env bash
set -euo pipefail
REPO="${1:-.}"
if [ ! -d "$REPO/.git" ]; then
echo "用法: $0 <agentscope-java 仓库路径>" >&2
exit 2
fi
echo "== 当前基线 =="
git -C "$REPO" describe --tags --always
git -C "$REPO" rev-parse --short HEAD
echo
echo "== 顶层模块 =="
grep -n "<module>" "$REPO/pom.xml" || true
echo
echo "== 关键符号落点 =="
for sym in HarnessAgent ReActAgent Middleware Toolkit AgentEvent; do
echo "-- $sym"
grep -rn --include='*.java' -l "$sym" "$REPO" \
| grep -v '/target/' \
| sed "s#$REPO/##" \
| sort \
| head -n 20
done
执行 bash scripts/source-map-check.sh /path/to/agentscope-java,会打印当前基线 commit、顶层模块列表,以及五个关键符号出现的文件清单。这张清单就是 IDE 走读的起点,不需要靠记忆。
第四步,写 docs/source-map.md。 这是本篇的核心产出:
# AgentScope Java 源码地图(基线 v2.0.3)
> 仓库:https://github.com/agentscope-ai/agentscope-java
> 基线 tag:v2.0.3
> 用途:数字人项目后续章节的源码入口索引
## 1. 模块地图
| 模块 | 路径 | 定位 | 核验命令 |
| --- | --- | --- | --- |
| core | agentscope-core | 能力内核:推理循环、模型适配、工具、事件 | mvn -pl agentscope-core dependency:tree |
| harness | agentscope-harness | 装配与运行时外壳 | mvn -pl agentscope-harness dependency:tree |
| extensions | agentscope-extensions | 可选集成 | mvn -pl agentscope-extensions dependency:tree |
| service | agentscope-service | 服务化与部署侧 | mvn -pl agentscope-service dependency:tree |
| examples | agentscope-examples | 用法验证 | 直接阅读 examples 源码 |
| docs | docs | 设计意图说明 | 只读,不当作行为证据 |
## 2. 核心符号表
| 符号 | 所属模块 | 职责 | 是否我的扩展点 |
| --- | --- | --- | --- |
| HarnessAgent | harness | 装配一次可运行的 Agent 会话 | 否,尽量不在这里写业务 |
| ReActAgent | core | 推理轮次控制 | 否,通过 Middleware 介入 |
| Middleware | core | 推理链切点扩展 | 是 |
| Model | core | 模型适配 | 视需求,通常通过配置切换 |
| Toolkit / Tool | core | 工具注册与调度 | 是 |
| AgentEvent | core | 对外事件通道 | 是,只做消费与观测 |
## 3. 关键调用链(待逐段落证)
HarnessAgent → ReActAgent → Middleware → Model → Toolkit/Tool → AgentEvent
| 段 | 进入文件 | 观察点 | 证据形式 |
| --- | --- | --- | --- |
| 1 | agentscope-harness/.../HarnessAgent.java | 会话启动时如何装配内核 Agent | 断点调用栈截图 |
| 2 | agentscope-core/.../ReActAgent.java | 一轮推理的入口 | 断点 + Call Hierarchy |
| 3 | core 下 Middleware 相关接口 | 切点顺序 | 断点命中顺序 |
| 4 | core 下 Model 适配层 | 请求如何交给具体 provider | 断点 + examples 对照 |
| 5 | core 下 Toolkit/Tool | 工具调用请求如何被调度 | 断点 + 单测对照 |
| 6 | core 下 AgentEvent | 事件如何向订阅者广播 | 运行页日志 |
## 4. 后续章节源码入口索引
| 章节 | 主题 | 首要源码入口 |
| --- | --- | --- |
| 第9式 | Tool / MCP | core 的 Toolkit 与 Tool 抽象 |
| 第10式 | 状态与 Harness | harness 的会话装配 |
| 后续 | Memory / Skill / Plan / Sandbox / 多 Agent | 以 core 为主,harness 为辅 |
注意表格里第 3、4、5、6 段的“进入文件”没有写死具体类路径,这是刻意的:类路径必须以你本地 tag 源码为准,写错了比不写更危险。
第五步,跑一次请求并把链跟完。 启动数字人 Demo 的 Spring Boot 服务,在浏览器打开 /run/{projectId},在 IDE 里依次下断点:先在 HarnessAgent 的会话启动路径下断点,步入到内核 Agent;再在 ReActAgent 的推理入口下条件断点,只停第一轮;然后在 Middleware 接口实现、Model 适配层、Toolkit 调度路径上分别下断点;最后在事件广播处下断点。用 Call Hierarchy(Ctrl+Alt+H)看上游,用 Find Usages(Alt+F7)看下游。
下面是示例输出,用于说明观察点的先后顺序,不是实测数值,也不代表你的工程一定有完全相同的调用顺序:
[probe] HarnessAgent 收到 /run/1001 的会话启动
[probe] -> 内核 Agent 进入第一轮推理
[probe] -> Middleware 切点命中
[probe] -> Model 返回文本片段
[probe] -> AgentEvent 推送文本增量
[probe] -> Model 返回工具调用请求
[probe] -> Toolkit 命中已注册 Tool
[probe] -> AgentEvent 推送工具结果
[probe] -> Model 生成最终答复
第六步,确认版本差异的判断方式。 在仓库里执行:
git fetch --tags origin
git diff --stat v2.0.3..origin/main -- agentscope-core
git log --oneline v2.0.3..origin/main -- agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java
git blame -L 1,60 v2.0.3 -- agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java
第一条告诉你 core 在 v2.0.3 之后动过哪些文件,第二条告诉你装配层有没有变,第三条告诉你某一行是哪次提交引入的。这三条命令合起来,就是“升级会不会踩到我”的初判依据。
第七步,提交。 在教程仓库从第7式分支继续创建本章分支:
git branch -a --list '*chapter/07*'
git checkout chapter/07-events # 换成上一步列出的真实分支名
git checkout -b chapter/08-source-code-map
git add docs/source-map.md scripts/source-map-check.sh
git commit -m "docs(ch08): map AgentScope Java modules and core call chain"
验收点:能从一次 /run/{projectId} 的请求出发,在 IDE 里至少跟到 ReActAgent、Model、Toolkit 和事件输出四处,并能用一句话说清 core 与 harness 的职责边界。
仓库先定基线版
断点一行一行跟
示例输出非实测
证据要落文件里
06、排查
这一章排查的不是线上故障,而是“读源码时最容易得出的错误结论”。三条证据链,全部来自真实阅读场景。
证据链一:以为改行为要动 HarnessAgent。
现象:想在推理前加一层业务校验,第一反应是在 HarnessAgent 里加代码。
怀疑:既然是“装配 Agent 的地方”,那它应该也管运行时行为。
检查:看依赖方向。在 agentscope-harness 的 pom.xml 里确认它依赖 agentscope-core;再反过来看 agentscope-core 是否依赖 harness。
证据:依赖是单向的——harness 依赖 core,core 不依赖 harness。这说明内核不知道装配层的存在,装配层只是把内核组装起来。
根因:把“装配”和“行为”混为一谈。装配层决定“用哪个模型、注册哪些工具、怎么启动”,行为由内核的抽象决定。
修复:业务校验写在内核提供的切点上(Middleware 一类抽象),不写装配层。设计文档里同步补一句“扩展点必须落在 core 的抽象上”。
证据链二:IDE 里点进去是反编译的 .class。
现象:在 Demo 工程里 Ctrl+左键点 ReActAgent,跳进去是一堆方法签名没有注释的代码,和 GitHub 上的源码对不上。
怀疑:官方是不是没开源这部分?或者 tag 和发布包不一致?
检查:先打开 ReActAgent.java(v2.0.3) 和 HarnessAgent.java(v2.0.3) 两个页面,确认源码在 tag 上是完整的;再在本地执行 mvn dependency:sources 看能否拉到 sources。
证据:tag 页面源码完整,说明不是“没开源”,而是构建产物没有绑定源码。
根因:IDE 索引的是依赖 jar,而不是源码模块。
修复:改用方式 A——把 agentscope-java 作为多模块工程导入,让 harness 和 core 都是源码模块。这样 Find Usages 和 Call Hierarchy 才能给出完整结果。
错误尝试: 直接在 Demo 工程里对 ReActAgent 按 Alt+F7 找调用点,看到结果里没有 HarnessAgent,就下结论“装配层不调用内核”。这个判断是错的。原因很具体:IDE 的 Find Usages 只索引当前已加载的源码与索引库。如果 harness 是以 jar 形式依赖进来的,索引里根本没有 harness 的源码,自然搜不到调用点。搜索结果为空,只说明“索引里没有”,不说明“代码里没有”。正确做法是先把依赖模块转成源码模块,再重新索引。
证据链三:把某个行为写死成“框架就是这样”。
现象:读 v2.0.3 的某段实现,得出一个结论并写进设计文档,几个月后升级发现不成立。
怀疑:是不是升级改坏了?
检查:git diff --stat v2.0.3..origin/main -- agentscope-core,再 git log --oneline v2.0.3..origin/main -- <你关心的文件>。
证据:如果该文件在 v2.0.3 之后被改过,说明这个结论只是一个时间切片上的事实,不是永久契约。
根因:把“某一版的行为”当成“框架的语义”。
修复:所有源码结论都必须带基线版本号。docs/source-map.md 里第一行就写死了“基线 v2.0.3”,就是为了强制这个习惯。
上游贡献判断: 本章排查的是读法问题,不涉及框架行为异常,因此不进入 Issue/PR 流程。是否可在最小 Demo 复现:不适用(没有异常行为);v2.0.3 是否存在:不适用;main 是否仍存在:不适用;是否已有 Issue/PR:写作时未检索到与“模块职责边界说明”直接对应的开放 Issue,如需引用请现场核验 Issues 与 Discussions;当前只能记录:如果核验后确认官方 docs 缺少模块依赖方向说明,可作为文档类 Issue/PR 候选,但不预设这个结论。
怀疑先要写下来
证据再去找一处
类名查得再勤快
不如测试跑一遍
07、优化
三条证据链指向同一个根因:把“看起来像”当成“事实上是”。修正方式不是更努力地读,而是把读法标准化。
标准化六步源码研究流程:
- 定基线。 先
git checkout v2.0.3,所有结论挂版本号。任何时候看到没有版本号的源码结论,都当作待核验。 - 定入口。 入口必须是一次真实请求,本次就是
/run/{projectId}。不从类名出发,从请求出发。 - 看依赖方向。 用
mvn -pl <module> dependency:tree和模块pom.xml确认谁依赖谁,先确定“哪一侧是稳定的”。 - 走一条链。 在 IDE 里断点 + Call Hierarchy 走完整条链,记录每一段的进入文件与观察点。
- 交叉验证。 用同一 tag 下的单元测试和
agentscope-examples验证你看到的调用关系。测试是第二份真相,examples 是第三份。 - 落文件。 结论写进
docs/source-map.md,并标注证据形式(断点、测试、examples、Release Notes)。
新行为: 从“按类名猜”变成“按调用链证”。任何一条涉及框架行为的结论,要么指得出源码位置,要么指得出测试位置;两条都指不出,就不写。
验证方法: 拿本式的完成标准反向验一次——随便挑一个第9式要用的结论,比如“Tool 注册后由内核调度”,看能不能在 docs/source-map.md 里找到对应行、能不能在 IDE 里重新走一遍。走不通,说明这张图还是猜的。
这一式之后,每篇文章涉及框架行为时,都要能落到相关官方源码或测试,而不是只引用文档。这条规则从本篇起生效。
流程定下不再猜
每一步都可复现
新行为要能验证
否则等于没优化
08、演进
[同一输入] 一次 /run/{projectId} 的会话请求
|
+--[V1] 黑盒调用:按文档和 examples 接线 / 代价:改动靠试错,升级不可控
|
+--[V2] 源码可追溯:断点走链 + 测试交叉验证 + 版本基线 / 代价:前期阅读成本
|
[Trade-off]
得到:可定位、可扩展、可判断升级风险;失去:短期“秒接”的速度;边界:只要你需要改框架行为,就必须走 V2
| 维度 | V1 黑盒调用 | V2 源码可追溯 |
|---|---|---|
| 正确性 | 靠 examples 的最短路径,边界情况无覆盖 | 靠调用链与测试,边界可查 |
| 稳定性 | 升级即高风险 | 升级前可用 diff 做初判 |
| 复杂度 | 前期低 | 前期高,需要一次完整走读 |
| 成本 | 短期低、长期反复返工 | 短期投入阅读,长期复用 |
| 适用范围 | 一次性验证、PoC | 会持续演进的项目 |
| 遗留问题 | 扩展点位置不确定 | 需要持续维护 source-map,防止文档过期 |
需要说清楚 V2 的代价:源码地图会腐烂。上游一次重构,地图就可能失真。所以 docs/source-map.md 必须带基线 tag,并约定“升级时同步更新地图”,否则它很快会变成新的误导源。
黑盒一时跑得快
白盒一次看得远
取舍不在快与慢
在于你要不要改
09、洞见
9.1 依赖方向决定扩展点位置
反直觉判断:你该在哪里写代码,不是由“哪个类看起来最相关”决定的,而是由依赖方向决定的。
harness 依赖 core,core 不依赖 harness。所以往 harness 里加业务,等于把业务绑在“装配方式”上;装配方式是最容易随版本变化的东西。相反,core 提供的 Tool、Middleware、事件这些抽象,是内核主动暴露的扩展面,向上稳定。第9式要加的自定义 Tool,就应该落在 core 的扩展面上,而不是落在 Spring Boot 的 service 层——service 层是业务编排,不是 Agent 的延伸。
9.2 类名是约定,不是契约
反直觉判断:类名越像描述行为,越容易骗人。HarnessAgent 听起来是“Agent 的壳”,实际上它是装配层;看到名字就推断职责,是读源码时最常见的错误起点。
可复用的做法是:任何结论先用三种证据交叉一次——源码位置、同 tag 的单元测试、agentscope-examples 的用法。三者中至少两处一致,才写成结论。这也是本系列后续所有章节的默认要求:类名不是证据,源码加测试加 Release 才是证据。
9.3 什么时候不应该做源码走读
这是本章最容易被忽略的一条。源码走读是有成本的,不是所有场景都值得做。
三种情况建议不做:一,只做一次性验证,跑通即弃,读 examples 足够;二,框架能力已经满足需求,你只是在接线,没有改造计划;三,时间盒内不可能落到代码改动上,读得再深也只会变成过期笔记。
判断标准很简单:你需不需要改行为。要改行为,走读是前置条件;不改行为,走读是过度投入。
9.4 测试是文档的第二份真相
文档描述设计意图,测试描述被验证过的行为,源码描述实际执行路径。三者中,文档最容易滞后,源码最容易误读,测试居中且最接近“约定”。
所以本章把“交叉验证”放在源码走读之后而不是之前。先自己形成假设,再用测试证伪,比先读测试再倒推结论更可靠——因为倒推出来的结论,你分不清是真懂了还是抄对了。
依赖向内不向外
命名向外不向内
能改的地方有限
看清了才不白改
10、系统落地
原来有什么:数字人项目在第7式结束时已经有一条能跑的链路——Web 管理端、/run/{projectId} 运行页、LiveKit 房间、Realtime Bridge、AgentScope Java 推理、事件推送到前端。链路上每个组件都能工作,但每个组件的内部都还是黑盒。
本篇新增什么:一张源码地图和一套读法。具体是 docs/source-map.md(模块地图、核心符号表、关键调用链、后续章节源码入口索引)和 scripts/source-map-check.sh(一条命令打印基线与关键符号落点)。不改任何业务代码,不新增任何运行时能力。
现在能做什么:拿到一次 /run/{projectId} 的请求,能在 IDE 里跟到 ReActAgent、Model、Toolkit 和事件输出;能说清 core 是能力内核、harness 是装配外壳;能在改动前先用 git diff v2.0.3..origin/main 判断升级风险。
还缺什么:地图是骨架,不是肌肉。Tool 的注册与调度细节、Middleware 的切点顺序、事件的具体类型与语义、会话状态如何被 harness 组织,这些都只标了入口,没展开。另外地图会腐烂,目前没有任何自动化手段保证它和上游同步。
下一步如何演进:第9式从 core 的 Toolkit / Tool 入手,把“数字人查项目配置变更记录”这个真实需求做成第一个自定义 Tool;同时把本式定下的规矩带过去——涉及框架行为的结论,必须落到源码或测试。
开源参与点:本章未发现明确上游问题。三条排查证据链分别是 IDE 索引机制、依赖方向和版本口径问题,都属于使用者的读法问题,不是框架缺陷。如果后续在做 Tool 时发现真实的上游行为异常,先在教程仓库做最小复现,再对比 v2.0.3 与 main,然后检索 Issues 与 Discussions,确认没有重复后再进入 Issue/PR 流程;参与规范见 CONTRIBUTING.md。
底座先有再长肉
本篇只加一张图
能力边界写清楚
后面章节才敢走
11、小结
Q1 → 类名只能证明命名者的意图,证明不了运行时行为。要证明行为,得看源码位置 + 同 tag 单元测试 + examples 用法三处交叉,两处一致才写结论。
Q2 → 文档和源码打架时,先信源码,再用测试确认,最后拿 Release Notes 解释为什么变了。文档描述意图,源码描述行为,两者不一致本身就是一条有价值的线索。
Q3 → 判断升级风险靠版本对比:git diff v2.0.3..origin/main 看改动面,git log 看提交内容,结合自己的扩展点位置判断影响。扩展点落在 core 抽象上时风险低,落在装配层上时风险高。
状态 → 数字人项目新增 docs/source-map.md 与 scripts/source-map-check.sh,分支 chapter/08-source-code-map,一次真实请求可在 IDE 中跟完 HarnessAgent → ReActAgent → Middleware → Model → Toolkit/Tool → AgentEvent。
[一次请求] /run/{projectId}
|
v
[装配] HarnessAgent(harness 模块)
|
v
[内核] ReActAgent → Middleware → Model → Toolkit/Tool(core 模块)
|
v
[出口] AgentEvent → Bridge → LiveKit → 运行页
一请求跟到末尾
四符号各归其位
能说清谁依赖谁
才算读完这一章
12、作业
12.1 理解题:为什么说“类名不是证据”?请用本章的一个具体例子说明。
参考答案:类名只是命名约定,不携带调用关系与依赖方向。例如 HarnessAgent 名字像“Agent 本体”,实际位于 agentscope-harness,是装配层;ReActAgent 位于 agentscope-core,是内核推理循环。仅凭名字推断会导致业务代码写错层,升级时被迫重写。正确做法是源码位置、同 tag 单元测试、examples 用法三处交叉验证。
12.2 实战题:在 v2.0.3 tag 上执行 scripts/source-map-check.sh,把你机器上的输出与本章表格对照,指出哪些行与你的核验结果不一致,并说明原因。
参考答案:重点看两处。一是顶层模块列表,应与 pom.xml 中的 <module> 完全一致;二是关键符号落点,HarnessAgent 应只在 harness 模块及其下游出现,ReActAgent 应出现在 core 模块。如果出现“core 里出现 HarnessAgent 引用”这类结果,先检查是不是把 main 分支和 v2.0.3 混在一起了——git describe --tags --always 的输出会告诉你当前到底在哪个基线。
12.3 排障题:在 IDE 里对 ReActAgent 按 Find Usages,结果里没有 HarnessAgent,可以直接下结论“装配层不调用内核”吗?
参考答案:不能。IDE 只索引已加载的源码与索引库;如果 harness 是以 jar 依赖形式引入的,索引里没有它的源码,搜索结果必然为空。这是“索引里没有”而不是“代码里没有”。修正做法是把 agentscope-java 作为多模块工程导入,让核验范围内的模块都成为源码模块,然后重新索引再搜。这个错误尝试本章已经记录过一次,属于最典型的误判路径。
12.4 架构判断题:数字人项目要加一个“查询项目配置变更记录”的自定义 Tool,代码应该写在 core 的扩展面上,还是写在 Spring Boot 的 service 层?请给出判断依据。
参考答案:查询逻辑本身写在 service 层,Tool 的注册与适配写在 core 的扩展面上。判断依据是依赖方向:core 是能力内核,被 harness 和业务层依赖;业务层是上层编排,框架不依赖它。把 Tool 的接入点放在 core 的扩展面(Tool / Toolkit 抽象)上,业务实现通过依赖注入从上层传入,这样升级时受影响面最小。如果整段逻辑直接写死在 service 层并由业务层自己调度模型,就等于绕开了内核的工具调度链,后续无法复用状态、事件和中间件能力。读者应打开 agentscope-core(v2.0.3) 目录与 agentscope-examples(v2.0.3) 中的相关示例代码,确认你写的接入点确实是官方支持的用法,而不是自造的层。
四道题各问一面
答不出就回去看
源码测试和发布
三样凑齐才算证
13、思考
本篇的核心冲突是:黑盒能跑通,但改不动。解法不是把框架读完,而是建立一套可复现的读法——定基线、定入口、看依赖方向、走一条链、交叉验证、落文件。这套读法本身也是可折旧的:地图会腐烂,所以它必须带版本号,必须跟着升级同步维护。
真正值得带走的判断只有一条:扩展点要落在稳定的那一侧。依赖方向决定了哪一侧稳定,类名决定不了。第9式要写的那个 Tool,写在 core 的扩展面上,三年后升级大概率还在;写在装配层上,一次重构就得重写。
“会用框架”是知道怎么调 API;“能参与框架演进”是知道 API 为什么长这样、边界在哪里、哪一行代码是你和上游共同维护的契约。这一式的全部意义,就是让你从前者走到后者的起点上。
文档说意不说实
源码说实不说意
两处对读才见真
会改框架才算懂

浙公网安备 33010602011771号