☰
AgentScope Java v2 第3课,从 ReActAgent 到 HarnessAgent
2026/10/17 6:45:49 网站建设 项目流程





开场:一句"没学到东西"砸出来的课

结论先说:HarnessAgent 不是第二台引擎,它就是上一课那个 ReActAgent,外面套了一层替你装好中间件的装备舱。我拿自己的 workbench 做了个控制变量换装实验:同一道题,同一个模型,事件流一条没多,input 涨到 8.2 倍。

这门课我上完本来是想吐槽的。第三课的主题是 Quickstart 精读,我原本以为就是带读一遍官方快速上手,结果上完只剩一句感想,原话记在笔记里:「我感觉没学到什么东西啊,就是改了一点代码?」后面还跟了两句:「至少把 ReActAgent 和 HarnessAgent 稍微讲一讲,也就是开个头,方便后面深入讲解」「ReActAgent 就只有这么一点内容吗?核心就是 Reasoning↔Acting 循环吗?我怎么感觉这个是 agentscope 核心呢?这个要确定清楚。」

先自限:这几句质疑,在当时的证据面前成立。Quickstart 五行起一个 agent,我的 workbench 从 ReActAgent 换到 HarnessAgent,改动拢共三处——bean 定义和 createAgent 两处返回类型放宽到 Agent 接口,builder 换个名字。看上去确实"就这点内容"。但吐槽不值钱,判决才值钱:是不是新引擎这种事,看文档下不了结论,得让事件流、账本和源码自己说话。

但实验跑完、648 行 SSE 逐条对完、2984 行源码翻完之后,我收回吐槽,换成三个结论。"改了一点代码"里我没学到的东西,恰好是三件:包装——它凭什么让事件流纹丝不动?装备——每轮多出来的 5.4k token 记在谁头上?手艺——Agent 接口的方法族,我一门都没亲手摸过。这篇按判决的顺序走:换装实验、膨胀算术、源码实锤、装备落在哪、家族全景、一条腿失明的翻车,最后补一场练兵。

换装判决实验:唯一变量是 agent 类

要判"HarnessAgent 是不是新引擎",变量必须只有一个。换装清单:两处返回类型放宽到 Agent 接口,builder 换成 HarnessAgent,builder 上那五连参数(含 maxIters(2))一字不动;DeepSeek 不换,题目也不换。maxIters(2) 压得低是故意的:额度小,打满额度的 RAW 收尾路径这次也必然走到——第二课见过的收尾形态,判决里要逐个复核。这么较真是因为换装顺手就会带进一堆干扰:改 builder 时顺手调参数、换个题、连模型一起换——那样测出来的差别,你说不清是壳的贡献还是顺手改动的贡献。判决实验的全部可信度,就押在"唯一变量是 agent 类"这一条上。

题目其实换过一次。第一版我抄了课件里的"现在几点",写完自己先看不下去,心里两句连珠炮:「怎么又问现在几点呢?这个工具轮次不够啊」「你看第二课,那个问题就很好,和代码就是契合的啊」。于是改回「帮我看看 MV 翔云轮最近一个航次的货物」——这道题逼模型走完整的 Reasoning→Acting 循环,结果还能跟第二课逐项对比。

跑之前我先把预测写下来,免得跑完之后处处"我早就知道"。四条预测,连押向一起落笔:事件类型会不会新增?——我押会,换了个"更高级"的 agent 总该有点新东西;CUSTOM 是否仍是常规轮加一?——我押是;打满额度的 RAW 直出还在吗?——我押在;input 涨多少?——这条真没底。

648 行 SSE 全贴没意义,只给裁决用得上的骨架(← 后面是我的读数,thread 273f021e / run 98f9fdfd):

R1(常规轮)REASONING → TEXT「我来查询这艘船的船期。」 → 2×并行 query_vessel_schedule(一个参数「翔云」,一个「翔云轮」) CUSTOM#1 input 6137, output 144, cached=0 ← 账单膨胀现场,下一节细算 R2(常规轮)REASONING → query_vessel_schedule("MSC AURORA") CUSTOM#2 input 6350, output 68, cached=6144 ← DeepSeek 前缀缓存吃掉大头 TOOL_CALL_RESULT MSC AURORA V-2609A 上海→鹿特丹 ETD 2026-10-03 RAW {"replyId":"","maxIters":2,"currentIter":2,"type":"EXCEED_MAX_ITERS"}

逐段读数。事件类型:一条没多,上一课见过的这课全有,这课有的上一课全见过。CUSTOM:3 条,公式照旧——常规轮 2 次,加打满额度后那轮 summary 收尾 1 次(第三条的真身下一节算账时就出场),CUSTOM = 常规轮 + 1。RAW 那行原样直出:replyId 空串、maxIters=2、currentIter=2、EXCEED_MAX_ITERS,跟第二课一个模子;RUN_FINISHED 正常收束。你多半会冒出这个疑问:换了个更强的 agent,事件怎么会一条不变?这个疑问先挂着,第 4 节源码里给答案——先把账算完。

对答案,逐条清算。第 1 条错——类型零新增,押"总该有点新东西"落空,这是全篇最大的意外;第 2、3 条对上——公式复现、RAW 原样,"照旧"不是扫一眼的感觉,是逐条对出来的;第 4 条出乎意料——不是涨多少的问题,是 8.2 倍:R1 的 input 6137,第二课同一道题是 748。半翻案:事件没胖,账单胖了。

R1 还有个插曲单独记一笔。我问的是翔云轮,模型第一轮先回「我来查询这艘船的船期。」,然后并行发了两个同名工具调用,一个赌「翔云」、一个赌「翔云轮」——船名它没见过,索性两头下注,全落空;第二轮转头去查数据里唯一存在的 MSC AURORA,一把命中。它从哪知道这个名字的?多半是第一发工具返回的班轮列表里就躺着(这一步我没逐字留痕,推断)。这是模型的自愈行为,跟 agent 类没关系——但它解释了第一轮事件流为什么"热闹"。排障时把两类行为分开:模型的行为找模型的原因,框架的行为找框架的原因。

多出的 5.4k,账本自己招了

8.2 倍这个数,先把来历摆清楚:本课 R1 的 input 6137,除以第二课同一道题的 748,等于 8.2。数从 CUSTOM 事件的 usage 字段里来,每轮模型调用一条,账本现成。我的第一反应是"系统提示变长了吧"——HarnessAgent 总得塞一段自我介绍。九成账不在这,账本自己会说话,而且零代码就能分解。

R2(常规工具轮,工具表在场) input 6350 R3(summary 轮,工具表不在场) input 1264 ────────────────────────────────── 6350 − 1264 = 5086,再加 R2→R3 间几百 token 的历史增量 ≈ 5.4k

R2 带着全套工具表;R3 是打满额度后的收尾作答,不占工具额度,请求里工具表是空的。两轮的其他条件几乎相同,一减,5.4k 的去向就出来了:内置工具的 JSON schema(R3 的历史其实更长,直接相减会把工具表算小,差的那截要加回来)。这刀能切,靠的是 R2 和 R3 构成一组对照——同一次实验、同一个会话、同一个模型,唯一差别是请求里带不带工具表。这套手法测过 Spring 的都熟:控制变量不只在实验设计里用,算账时一样好使。顺手对个总账:开场那笔 6137 − 748 = 5389 ≈ 5.4k,跟这里算出的 5.4k 互为印证——工具表的体积正好解释全部差值。18 个内置工具类——FilesystemTool、ShellExecuteTool、Memory 三件套、TaskTool、SkillManageTool、PlanModeTools、WebTools、AgentSpawnTool,等 18 个——每个都要把自己的 schema 塞进每一轮请求。你一行工具代码没写,这笔钱照付。

系统提示的份额也切过一刀:summary 轮的 input,第二课是 386,这课是 1264,差 878,量级上对得上系统提示注入。再往下切——每条提示各占多少——账本切不出来,我只能给到"878 ≈ 系统提示"这个口径,再细就是猜了。

缓存那头 DeepSeek 倒诚实:R2 的 6350 里 6144 命中前缀缓存,真正新增只有两百来 token。8.2 倍是"请求要算多少",不完全是"你要付多少"。

还有一个数,写到这篇我也没想通:R3 的 input 1264,cached=768。把两课的 summary 轮并排放更扎眼:第二课 input 386、cached 0;这一课 input 1264、cached 768。换装换了请求形态,前缀缓存按理该全丢,怎么反而命中了 768?我不知道。记下来,标上"悬案",留给下一课的模型抽象,这条我不硬编解释。

打开引擎盖:三处实锤

"包装"两个字不是我抠出来的,官方 javadoc 上就这么写。我把 HarnessAgent.java 整个拉下来——2984 行、135KB,存进 .scratch 里慢慢翻——这就是"Quickstart 五行"的真实尺寸。三处实锤逐个亮,每一处都带行号,想复核的自己对着源码翻。

实锤一,javadoc 自述(L145-149):“HarnessAgent is the user-facing harness API that wraps a ReActAgent with workspace / filesystem / sandbox / subagent / skill / plan-mode / MCP”。紧跟着一句选型指南:“For plain ReAct usage without any of the above, useReActAgent.builder()directly.”——上面这些装备你都不要,就直接用裸的 ReActAgent.builder()。

实锤二,Builder 的第一个字段(L1209):

privatefinalReActAgent.Builderinner=ReActAgent.builder();

你在 HarnessAgent.builder() 上填的五连参数,原样转发给这个 inner。壳不存参数,它只是替你先把引擎的 builder 备好了。

实锤三,包装本体(L1004):

privateFlux<AgentEvent>wrappedStreamEvents(RuntimeContexteffective,Supplier<Flux<AgentEvent>>inner){returnFlux.using(()->{// acquire:进场领装备if(sandboxLifecycleMw!=null){sandboxLifecycleMw.acquireForCall(effective);}returneffective;},eff->inner.get(),// 放行:整条流交给 ReActAgenteff->{// release:出场还装备if(sandboxLifecycleMw!=null){sandboxLifecycleMw.releaseForCall(eff);}});}

写 Spring 的看这个结构会直接认出来:@Around 通知。方法调用前进场,proceed() 放行,返回后出场——HarnessAgent 的每次调用就是一次被代理的执行:进场办装备,中间 inner.get() 把整条事件流原封不动交给引擎,出场还装备。它顺手回答了"换了更强的 agent,为什么事件一条没变":中间那步是透传,循环还是那个循环,一个字节没改。类比到此为止——@Around 拦得住进出,拦不住循环本体。

javap -p 把私有字段全录一遍,第一行就是private final ReActAgent delegate。两类 agent 的差别收拢成三条。①入口多一层包装:每次调用先过 wrapped* 办装备(领沙箱上下文、挂 workspace 文件系统),再交 delegate 跑循环,事件从引擎里出来还是原来那批,壳不加工事件。②出厂预装中间件:ReActAgent.Builder 本来就有 hook()/middleware(),扩展点一直都在;HarnessAgent 做的事是替你把压缩、沙箱、子代理装好、装对——你少写的不是功能,是装配工序。③生命周期模型:workbench 的裸引擎每请求 new 一个,这是第二课的写法;HarnessAgent 的官方定位是单例长命,javadoc 原话 “stateless between calls, safe to use as a singleton”,内部靠 (userId, sessionId) 隔离,同会话串行、跨会话并行。一个每请求换新,一个长命常驻,这是用法上真正的分水岭。辨析这两类只消一问:循环在哪跑——答案里带 delegate/inner 的,是包装;答案就是它自己的,才是引擎。

官方 Quickstart 把推荐位给了 HarnessAgent:“HarnessAgent is the recommended entry point”,理由是它 “packages workspace, long-term memory, session persistence, subagents, sandboxes, and other engineering capabilities into one builder”——packaging 句里那串名词,跟 javadoc 的能力清单高度重叠,各少了对方几样。但同一套文档里留着另一条路:“If you only need theReActAgentreasoning, tool and context APIs and want to compose the application capabilities yourself,agentscope-coreis enough”。两句都对:推荐的是省事,直接用的是干净,判断归你。顺带一提,Quickstart 页面的 builder 示例还带着一个本地 2.0.3 没有的参数——文档又漂了,第二课的立场这课续用:以本地源码为准。

装备在哪:开关与房子

javadoc 里那串 workspace、memory、sandbox、subagent,落到代码里是 Builder 上一排开关:.workspace(Path)、.memory(…)、.subagent(…)、.enablePlanMode(),外加成排的 disableXxx()——disableFilesystemTools、disableMemoryTools、disableCompaction、disableSubagents。每个开关背后的机制各归后续课,这篇只管两件事:开关在哪,代价多少。代价第 3 节刚算完,位置就在这。顺带记一个默认值:计划模式默认是关的;其余开关谁默认开、各自管什么,后续课逐个拆。

配置之外还有磁盘。实验跑完我翻了一趟目录:

.agentscope/workspace/ ├── 273f021e-d3a4-4694-9ed9-3f199a664c1a/ ← 实验线程 │ ├── agents/ │ ├── default/ │ ├── memory/ │ └── MEMORY.md ├── smoke-0003/ └── smoke-ds-1/

按会话分室,MEMORY.md 躺在自己那间室里——上一节那条 (userId, sessionId) 隔离,落到磁盘上就是这个形态。smoke-0003 和 smoke-ds-1 是练兵冒烟的线程,也各占一室:磁盘上没有全局大杂烩,按 threadId 一室一户。5.4k 的工具表和这片目录是同一套装备的两面:一面每轮塞进请求,账单上看得到;一面落在磁盘上,目录树里看得到。

这些开关也不是摆设。想验证 5.4k 到底记在谁头上,一个 .disableFilesystemTools() 探针跑一发,前后两笔 CUSTOM 一对,账单变化立等可见,零成本验证。

家族全景:能 new 的只有四个

这两类之外还有别的 agent 吗?我把两个 jar 全量扫了一遍。扫描口径说一句:带 Agent 字样的类不少,但多的是外围角色,能直接 new 出来当 agent 用的实现只有四个。

表后补两句。UserAgent 值得多看一眼:它把"人"建模成 agent——流水线上需要人工出手的那个工位,也做成 agent,能进 MsgHub(多 agent 消息中枢,后续课拆)和机器 agent 平起平坐。连人都当成一种 agent,这件事本身就说明家族的底盘有多统一。另外,子代理和 teams 不是新的 agent 类,是声明与编排——编排得再花,跑的还是那台引擎。四个里有两个其实是同一台机器的两个入口,getDelegate() 一调就现形。这就接到开场那个问题的前半:这四个类里块头最大的是 HarnessAgent,但框架的核心不是块头最大的那个、也不是名字最唬人的那个,是被包装的那个:真正的模型驱动引擎只有一个,ReActAgent。开场我问"核心就是 Reasoning↔Acting 循环吗",这一半现在可以答:是。后半——循环之外这台框架还押了什么——留到收口。

一字之差,一条腿失明

换装在 AG-UI 腿全程跑通之后,我飘了。workbench 是双端点接入,另一条腿是 OpenAI 兼容的 chat-completions——我押的是 200 照常回答:同一个 bean,两条腿凭什么不一样?押注理由现在说出来也不丢人:bean 在容器里活着,AG-UI 腿每请求都拿得到,换个入口怎么就不行。后来对答案才看清,这是把"bean 存在"当成了"bean 可取",正好踩在声明类型这个坑上。

curl 一发打脸(就是这条命令,-w 后面是读数格式串,下文那行读数就是它打出来的):

curl-s-XPOST http://127.0.0.1:8080/v1/chat/completions\-H"Content-Type: application/json"\-d'{"model":"<你配置的模型名>","messages":[{"role":"user","content":"hi"}]}'\-w'HTTP=%{http_code} SIZE=%{size_download} TIME=%{time_total}s\n'
HTTP=500 SIZE=5970 TIME=0.267572s {"status":500,"error":"Internal Server Error", "trace":"java.lang.RuntimeException: Failed to process request at io.agentscope.spring.boot.chat.web.ChatCompletionsController .createCompletion(ChatCompletionsController.java:200)"}

0.27 秒就回来,快得根本没出网——零模型调用,死在 bean 装配这一层。trace 指名道姓:createCompletion 第 200 行,异常在 controller 里就抛了,压根没走到模型那一步。

源码裁决。chat-completions 腿的 controller 注入ObjectProvider<ReActAgent>,按声明类型找 bean。我换装时把返回类型放宽成了 Agent 接口——上下文里确实有一个 agent bean,但声明类型是 Agent;HarnessAgent 也不是 ReActAgent 的子类。候选数 0,500。AG-UI 腿能进,是那边的 registry 收Supplier<Agent>——收接口,谁实现谁进。

同样的代码,两条腿一通一死。说"一字之差"不夸张:差别就在注入声明写的那个类名——把 controller 里的ObjectProvider<ReActAgent>改成ObjectProvider<Agent>,这条腿当场复活,动的就是声明里的一个类名。Spring 找 bean 认的是声明类型——ObjectProvider<ReActAgent>要的是能以 ReActAgent 身份交出来的 bean,实例能不能干活不重要,类层次对不上就是没有。启动时为什么不报错?这条腿的注入走的是 ObjectProvider,按需解析:应用照常起来,一发请求才现形。这次翻案记成两句话:迁移方向没错,但两条腿契约不同;往后排查,两条腿行为不一致,先别看模型、别看配置,先看各自注入点按什么类型找 bean。

补一场练兵:手艺层与三个新发现

上半场结束,回头再看那句"没学到什么东西",我认了一半:问题不全在课,在我的手。Agent 接口的方法族我一门没亲手摸过——会修发动机不等于会开车,上半场修明白了发动机,下半场补开车。这半场没有判决要下,手放上方向盘就行。回头看,这一天其实转了三次向:实验题从"现在几点"改成货代链,薄感反馈改出了这半场练兵,追问源码逼出了上半场那三处实锤——每次转向都不是计划内的,但每次都改对了地方。

练兵先解决我自己冒出的一个疑问。看到练兵代码挂着 fake:fake 免 key,我的原话是:「为什么要 fake:fake 免 key?是为了效果吗?你和不调用 LLM 吗?也不需要你节省这点 token 啊。」(笔记原样照录,毛糙处不动。)跑完两轮我认了:fake 不是为省钱,是为了逐字节可对比——机制验证要的就是每次运行一模一样,真模型每次回复都不重样,当不了标尺。但 fake 有条硬边界:它不读消息内容。所以练兵类写成模型无关,一个 USE_FAKE 开关切换:fake 练机制确定性,deepseek 练内容读取。

四门手艺签名速览(Mono≈单个最终值、Flux≈事件流,都是 Reactor 的响应式类型):

  • call(List) → Mono:消费全部事件,只交最终结果
  • streamEvents(List) → Flux:过程和结果都在流里
  • observe(Msg) → Mono:注入上下文、不触发推理,结构上就没有事件流
  • call(List, Class):结构化输出,解析结果埋在 metadata 里

怎么选,从签名就能读出来:只要最终答案用 call;要把思考、工具调用一步步透给前端用 streamEvents;往会话里塞上下文又不许惊动模型用 observe;要机器可读的输出用带 Class 的 call。两轮 5/5 绿,4.5s 加 1.3s。两轮的 Skipped 数不同,跳的是对方组专属的用例——同一套测试,两种模型各跑各的组。关键输出摘出来(e* 是测试里的取证编号,e1 拿结果、e2 数事件、e3 看 observe、e4 看结构化输出):

── 第 1 轮 USE_FAKE=false(deepseek 半场)Tests run: 5, Failures: 0, Errors: 0, Skipped: 3, Time elapsed: 4.545 s [e3b] 回答=夜莺 [e4] metadata 值类型=java.util.LinkedHashMap [e4] 值={urgent=true, vesselName=MSC AURORA} ── 第 2 轮 USE_FAKE=true(fake 半场)Tests run: 5, Failures: 0, Errors: 0, Skipped: 2, Time elapsed: 1.319 s [e1] role=ASSISTANT text=时间查询完成: 2026-09-29 23:11:58 [e3a] 历史 1 条: [e3a] USER: 记住暗号:夜莺

发现一,从 [e3b] 读起。暗号是我在进程里 observe 进去的:「记住暗号:夜莺」。observe 只塞历史不触发推理——按理解,这条消息躺在历史里,没人碰它。可下一问"暗号是什么",模型张口就是:夜莺。它从哪知道的?消息没出过我的进程。拆开看:observe 把这条 Msg 塞进了会话历史,下一次 call 请求时整段历史原样进 prompt——不是模型通灵,是我自己把暗号递过去的,只是没看见递的动作。[e3a] 还补了一刀:observe 之后历史只有 1 条,系统提示不在里面——sysPrompt 不入史,请求时才现拼。fake 半场同一段历史摆着,[e1] 那句「时间查询完成: 2026-09-29 23:11:58」是脚本写死的罐头,暗号它永远不会知道。同一段历史,读不读,天壤之别,硬边界实锤。

发现二,[e4]。我让模型按 record 的 schema 返回结构化结果,metadata 里捞出来的是 java.util.LinkedHashMap,不是 record 实例:{urgent=true, vesselName=MSC AURORA}。取的位置也有讲究:结果不在返回值里,要按 MessageMetadataKeys.STRUCTURED_OUTPUT 这个 key 去 metadata 里捞。record 只是 schema 指导,落地就是个 Map——拿它当强类型用之前,先自己校验。

发现三最厚。为什么非要直采?AG-UI 面板上看到的事件名都经过了转换器,要知道引擎原生发的是什么,只能自己到 core 层听一遍。我绕过转换器直采核心事件流,逐类计数,16 行:

[e2] AgentEndEvent × 1 [e2] AgentResultEvent × 1 [e2] AgentStartEvent × 1 [e2] ModelCallEndEvent × 2 [e2] ModelCallStartEvent × 2 [e2] TextBlockDeltaEvent × 1 [e2] TextBlockEndEvent × 1 [e2] TextBlockStartEvent × 1 [e2] ThinkingBlockDeltaEvent × 1 [e2] ThinkingBlockEndEvent × 1 [e2] ThinkingBlockStartEvent × 1 [e2] ToolCallEndEvent × 1 [e2] ToolCallStartEvent × 1 [e2] ToolResultEndEvent × 1 [e2] ToolResultStartEvent × 1 [e2] ToolResultTextDeltaEvent × 1

核心层命名是"块家族":Thinking、Text、ToolCall、ToolResult,每块一条 Start/Delta/End 生命周期(ToolResult 的 Delta 落在文本上,名带 TextDelta)。清单里还有两处对得上前文:ModelCallStart/End 各是 2——两轮推理两条 CUSTOM,AG-UI 那边的账就是从这个事件上长出来的;AgentStart/End 各 1——第二课"三种收场"里的正常收束走的就是这一族,练兵这轮没打满、没中断,收在它手上。练兵我考过自己一次:AG-UI 那套族名还记得几个?我答了 REASONING_MESSAGE_START、REASONING_MESSAGE_END,「还有一个处理中吧,我不太记得了」——三个对俩,CONTENT 忘了,但"处理中"这个直觉方向是对的。对完这张表,蒙对的有了解释:

表后清两笔旧账。第一课问过的旧账——fake 腿为什么一条 token 账单都没有——这课看清了:core 的 ModelCallEndEvent 带着 usage,转换器只在 usage 存在时把它转成 CUSTOM,来源和条件都对上。第二笔是族名钩子:core 叫 Delta,AG-UI 叫 Content,一个意思两个名字——口测那个"处理中",方向对了,下次不用蒙。

收口:带走三样,临走三问

开场那几句「就是改了一点代码?」「核心就是 Reasoning↔Acting 循环吗?」现在都能完整回答了。改的确实只有三处,但三处背后压着三件事:包装——HarnessAgent 是同一台 ReActAgent 套的壳,事件流零新增是透传的直接后果;装备——壳替你预装的工具表和磁盘分室,每轮多收约 5.4k 的账;手艺——Agent 接口的方法族不亲手摸一遍,换什么壳都一样薄。至于「把两个 agent 稍微讲一讲」——第 4、6 两节就是答卷;第 6 节留的那半问(循环之外还押了什么),答案就是装备与手艺这两件。

带走三样。选型:要完全控制、自己装,官方选型原话站台,ReActAgent.builder() 直接用;要开箱即用,上 HarnessAgent,代价是背装备账,拿不准就挂一个 .disableFilesystemTools() 探针跑一发,账单变化立等可见。排障:两条腿行为不一致,先查各自按什么类型找 bean;模型的自愈(翔云赌空转头 MSC AURORA)跟 agent 类无关——换个壳还能复现,才归模型;两类分开归因,别让模型背框架的锅。算账:CUSTOM 账本是零代码分解器,有工具表的轮减去没工具表的轮,差值就是装备价签。

复现三步:一,在上一课的 workbench 上做三处换装——bean 定义和 createAgent 两处返回类型放宽到 Agent、builder 换 HarnessAgent——builder 上那五连参数(name、sysPrompt、model、toolkit、maxIters(2))一字不动,签名以本地 Builder 为准,编译不过先核对它;二,用第二课的 AG-UI 面板发「帮我看看 MV 翔云轮最近一个航次的货物」,先写预测再实测,对照事件类型、CUSTOM 条数、input token 的量级(本文是 8.2 倍,你的数会是自己那次运行的);三,curl 打 chat-completions 腿一发最小请求(第 7 节那条命令),预期 500。万一两腿都不通:先用 fake:fake 确认 agent 本身能跑,再查端口和 key。三步走完,那个 500 和装备账会自己找上门。

临走三问,只给场景不给答案。一,同事说"我换上 HarnessAgent,事件流一条没变,它就是个空壳"——这个观察支持他的结论吗?你会让他回 648 行 SSE 里找哪个字段当铁证?二,账单每轮多出约 5.4k input,第一反应是"系统提示变长了"——不开 debugger,只用工具轮和 summary 轮两笔 CUSTOM,怎么算出大头是谁的?三,同一条腿上的两个入口,AG-UI 腿好端端,chat-completions 腿 500,启动日志干干净净——第一个该看的地方是哪?

三道都答对了,这一课才算真的带走。

系列走到第三课,进度 3/56。下一课是模型抽象,开场白都定好了——就从 summary 轮那笔 cached=768 的悬案开始:换了请求形态,前缀缓存按理该全失,为什么还命中 768?你可以先猜,猜什么都行,下一课开头先对这个数。

AgentScope Java v2 第3课,从 ReActAgent 到 HarnessAgent

2026.10.12 · 阅读约 14 分钟

如果这篇文章对你有帮助,欢迎转发给需要的同事和朋友。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询