一个 Markdown 文件就是一个员工 —— AgentScope-Java 多智能体编排实战(核心篇)

P3:一个 Markdown 文件就是一个员工 ------ AgentScope-Java 多智能体编排实战(核心篇)

系列第三篇。P1 跑通了单个 Agent,P2 让它长出了双手(工具调用),这一篇解决真正的问题:一个 Agent 干不了的活,怎么让一队 Agent 分工干。

写作备注(发布前删除本节):

  • 全文约 3400 字,5 处截图占位(【图1】~【图5】)
  • 代码全部取自仓库真实实现:agent/MultiAgentDemo.java、.agentscope/workspace/agents/*.md
  • 发布前填入 GitHub 仓库地址;【图4】注意脱敏(见文末检查项)
  • 本文是全系列核心篇,篇幅最长,建议同步分发知乎/掘金时拆出「temperature 差异化」一节做独立小标题

你能获得什么

  • 理解 AgentScope-Java 的多智能体编排模型:orchestrator-workers(总编-worker 模式)
  • 学会用 一个 markdown 文件声明一个子 Agent------不改一行 Java 代码就能给团队加人
  • 看懂一次完整的多 Agent 协作流程:派活 → 隔离执行 → 结果回传 → 整合简报
  • 拿到一个可直接复用的「内容团队」模板:大纲、撰稿、审校三个角色,temperature 各不相同

【图1:文章封面/架构图】

一、为什么单个 Agent 不够用

P2 里我们的 Agent 已经会调工具了,但你很快会遇到三个天花板:

  1. 上下文污染。让一个 Agent 又查资料、又写初稿、又挑自己的错,它的上下文会被塞满中间过程,写到后半段质量明显下滑------它一边写一边被自己前面的草稿干扰。
  2. temperature 无法兼得。写初稿要发散(温度高一点才有创造力),审校要严谨(温度低一点才稳定)。一个 Agent 只有一个温度,鱼和熊掌不可兼得。
  3. 职责不可复用。审校逻辑写死在系统提示词里,下次做个「代码审查助手」还得重写一遍。

解法很经典:orchestrator-workers 模式。一个总编 Agent 负责理解需求、拆任务、派活;每个 worker 是独立的小 Agent,有自己的系统提示词、自己的温度、自己的上下文,干完活只回传结果。这和微服务里的「API 网关 + 微服务」是同一个思想------只不过这里的「服务」是会推理的。

AgentScope-Java 把这套模式做成了开箱即用的能力,而且声明一个 worker 只需要一个 markdown 文件。

二、核心机制:agents/ 目录就是团队花名册

先说结论:在 workspace 下的 agents/ 目录里放一个 markdown 文件,就等于雇了一个新员工。

文件分两段:YAML front matter 定义「人事信息」,正文就是系统提示词。以我们内容团队的 outliner(结构策划)为例,这是仓库里 .agentscope/workspace/agents/outliner.md 的原文:

markdown 复制代码
---
name: outliner
description: 文章结构策划。把一个写作主题展开成结构化大纲,含开头钩子、核心论点、案例建议与结尾。需要列大纲时派给它。
temperature: 0.7
---
你是一个资深的公众号文章结构策划,擅长把一个主题拆解成有传播力的结构。

用户会给你一个写作主题,你需要输出一份可直接执行的大纲:
1. **开头钩子**:一句能让人停下来读下去的话(痛点提问 / 反常识结论 / 数据冲击,三选一)
2. **核心论点**:3 个,每个配一句展开说明
3. **案例与素材建议**:每个论点配什么代码、截图或数据
4. **结尾**:收束方式 + 下期预告钩子

要求:只输出大纲本身,不要写正文,不要多余解释。

front matter 里各字段的门道:

字段 作用 注意
name 员工工号,总编派活时的「收件人」 必填,目录内唯一
description 岗位 JD------总编就是靠它决定「这活该派给谁」 必填,且要写清「什么时候派给它」
temperature 这个角色的发散程度 按职责定,见下文
tools 该角色可用的工具名列表 按名引用父级 Toolkit 注册的工具,不写则没有工具

description 是最容易被低估的字段。总编自己不认识这些 worker,它选人完全依赖 description------写得含糊(比如只写「帮我干活」),总编就会乱派活。正确姿势是把 description 当 JD 写:职责 + 触发条件。上面 outliner 的描述里「需要列大纲时派给它」就是在给总编下路由规则。

再看 writer 的声明,注意它的 tools:

markdown 复制代码
---
name: writer
description: 撰稿人。根据大纲或要点撰写公众号文章初稿,风格专业但易读,善用短段落和小标题。需要写初稿时派给它。
temperature: 0.8
tools:
  - save_draft
---
你是一个技术公众号的撰稿人,文风专业但不枯燥:短段落、多小标题、关键处给代码或例子。
......
如果用户要求「保存」,调用 save_draft 工具把初稿存入草稿箱,标题取文章标题。

save_draft 是 P2 里我们用 @Tool 写的自定义工具(存在草稿箱)。父子工具打通的方式:在总编的 Toolkit 里注册一次,子 Agent 在声明里按名引用。也就是工具只写一遍,全团队共用。

三个角色的 temperature 各不相同,这是我们刻意的设计:

角色 temperature 理由
outliner 0.7 出大纲要点创意,但不能太飘
writer 0.8 写初稿最需要发散
reviewer 0.3 审校要稳定、可复现,温度高了会「看走眼」

这就是第二节说的「一个 Agent 无法兼得 temperature」的解法------把不同温度拆给不同角色。

【图2:agents/ 目录三个 md 文件(资源管理器 + 任一文件内容同框)】

三、总编怎么搭:一个 builder 搞定

worker 是 markdown 声明的,总编则在 Java 里构建。完整代码来自仓库的 MultiAgentDemo.java:

java 复制代码
// 1. 父级 Toolkit:注册自定义工具,子 Agent 通过声明中的 tools 列表按名引用
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new DraftTools(Paths.get(".agentscope/workspace/drafts")));

// 2. 总编:workspace 下的 agents/ 目录即「团队花名册」
HarnessAgent editorInChief = HarnessAgent.builder()
        .name("editor-in-chief")
        .sysPrompt("""
                你是一个公众号内容团队的总编,手下有三员干将:
                - outliner:结构策划,负责出大纲
                - writer:撰稿人,负责写初稿(可把成品存进草稿箱)
                - reviewer:审校,负责挑错

                工作流:接到需求后,先派 outliner 出大纲;大纲满意后派 writer \
                写初稿;初稿完成后派 reviewer 质检。每一步必须等上一步结果回来再派下一棒。\
                最终把各环节结论整合成一份简报回复用户。""")
        .model(ModelFactory.build())
        .workspace(Paths.get(".agentscope/workspace"))
        .toolkit(toolkit)
        .compaction(CompactionConfig.builder()
                .triggerMessages(30)
                .keepMessages(10)
                .build())
        .build();

四个关键点:

  1. .workspace() 指向的目录里如果存在 agents/ 子目录,框架运行时会自动扫描加载,不需要手动注册任何子 Agent。这就是「声明式」的含义:加一个新员工 = 加一个 md 文件,Java 代码零改动。
  2. 总编的 sysPrompt 里写工作流,而不是写每个环节怎么做------每个环节的专业知识在各自 worker 的提示词里。总编只管「谁先谁后、等上一步结果」。
  3. .compaction() 配了上下文压缩:超过 30 条消息时压缩只保留最近 10 条。整场编排下来事件很多,这个配置防止长会话把上下文撑爆(P4 会专门展开)。
  4. 没有任何一行代码涉及 agent_spawn 等编排工具------框架默认给 HarnessAgent 自带了一整套编排工具 (agent_spawn / agent_send / agent_list,连同文件读写、memory、session 系列共 21 个内置工具),总编开箱就会「派活」。

四、跑起来:一次完整的三棒接力

给总编发一个需求:

java 复制代码
UserMessage message = new UserMessage(
        "选题:《Java 开发者如何零 Python 基础搭建多智能体系统》。"
                + "请按团队流程走一遍:出大纲 → 写初稿 → 审校,最后给我一份执行简报。");

用 P1 讲过的 streamEvents() 监听事件流,把「派活」和「回传」的过程打印出来:

java 复制代码
editorInChief.streamEvents(message, ctx)
        .doOnNext(event -> {
            switch (event.getType()) {
                case TEXT_BLOCK_DELTA ->
                        System.out.print(((TextBlockDeltaEvent) event).getDelta());
                case TOOL_CALL_START ->
                        System.out.println("\n[委派] " + ((ToolCallStartEvent) event).getToolCallName()
                                + " ← 总编正在派活");
                case TOOL_RESULT_END ->
                        System.out.println("\n[子Agent回传] " + ((ToolResultEndEvent) event).getToolCallName());
                default -> { }
            }
        })
        .blockLast();

运行命令:

bash 复制代码
mvn -q exec:java -Dexec.mainClass="com.mk.agentscope.agent.MultiAgentDemo"

真实运行的控制台输出(节选)长这样:

text 复制代码
=== 多智能体编排演示 ===
子 Agent 声明目录:.agentscope/workspace/agents/

[委派] agent_spawn ← 总编正在派活
[子Agent回传] agent_spawn:
一、开头钩子:「你的同事在用 Python 写 Agent,而你还在写 CRUD?」......

[委派] agent_spawn ← 总编正在派活
[子Agent回传] agent_spawn:
# 让 Agent 长出双手:......(初稿正文)

好,我用 save_draft 存一下草稿。

[委派] agent_spawn ← 总编正在派活
[子Agent回传] agent_spawn:
P0 必须改:第 2 节「Schema 自动生成」表述不严谨......

【执行简报】大纲已确认;初稿已存入草稿箱(drafts/p3-xxx.md);
审校提出 1 个 P0 问题、2 个 P1 建议,已附修改清单。

注意三个细节:

  • 三次 [委派] 都是 agent_spawn------在总编眼里,派活就是一次普通的工具调用。多智能体编排没有引入任何新的执行模型,就是 ReAct 循环里多了一种「 spawning 子 Agent」的工具。这个设计非常干净。
  • writer 回传里出现了 save_draft 调用------子 Agent 在隔离上下文里自主干活,中途调用自己的工具,总编全程无感,只拿到最终结果。
  • 最后的「执行简报」是总编自己整合的------它不转发原始长文,而是汇总各环节结论。这正是 orchestrator 的价值:信息向上收敛。

【图3:控制台完整输出截图(三棒接力的委派与回传)】 【图4:草稿箱目录截图------writer 真的通过 save_draft 落盘了初稿文件】

五、背后发生了什么

把这次运行拆开看,一次三棒接力的完整时序:

text 复制代码
用户需求
  │
  ▼
总编(editor-in-chief,ReAct 循环)
  │ ① 推理:这活该 outliner 干 → 调 agent_spawn("outliner", 任务)
  │      └─ outliner(temp 0.7,隔离上下文)→ 大纲文本回传
  │ ② 推理:大纲 OK → 调 agent_spawn("writer", 大纲)
  │      └─ writer(temp 0.8,隔离上下文)→ 初稿 + 调 save_draft 落盘
  │ ③ 推理:初稿 OK → 调 agent_spawn("reviewer", 初稿)
  │      └─ reviewer(temp 0.3,隔离上下文)→ 分级问题清单回传
  │ ④ 推理:信息齐了 → 整合执行简报
  ▼
最终简报返回用户

几个值得说的机制:

  • 隔离上下文:每个 worker 的对话发生在自己的上下文里,大纲讨论的 30 条消息不会进入 writer 的视野,writer 只拿到「大纲成品」。这就是解决「上下文污染」的方式。
  • 运行期加载 :agents/ 目录的扫描发生在首次推理时(由框架的中间件完成),而不是 builder 阶段。好处是声明文件可以随时增删,下一轮对话即刻生效------「团队花名册」是活的。
  • 想验证声明文件写得对不对 ,不用起整个 Agent:AgentSpecLoader.loadFromDirectory(agentsDir, workspace) 可以单独调用,直接拿到解析后的声明对象做单测。CI 里加一条检查,markdown 写错了在构建期就暴露。

六、四条编排设计经验

  1. worker 的 description 按 JD 写:职责 + 触发条件。它是总编派活的唯一依据。
  2. 专业知识下沉到 worker,工作流上浮到总编。两边别写串:总编提示词里出现「大纲要包含钩子」就是越权。
  3. temperature 按职责分配 :创意型角色 0.70.9,质检型角色 0.10.3。这是多 Agent 相对单 Agent 最实在的收益之一。
  4. 控制回传体积:worker 结果别原样倾倒给总编,让 worker 自己先收敛(reviewer 输出分级清单而不是全文批注),或像 Demo 里那样对回传做截断预览。回传太长会快速吃掉总编的上下文。

七、踩坑记录

坑 1:静态声明目录是 agents/,不是 subagents/。 我们在搭建时先按直觉在 workspace 下建了 subagents/,死活不生效。翻框架源码常量(WorkspaceConstants.AGENTS_DIR = "agents")才发现:静态声明的目录名是 agents/ ,subagents/ 是运行期动态生成子 Agent 用的另一个机制。网上搜到的早期教程多写的是后者,注意区分。

坑 2:front matter 里 description 漏写不报错,但总编会乱派活。 声明文件解析对缺失字段相当宽容,漏了 description 也能加载成功。但总编路由全靠它------缺失后可能出现「该派 reviewer 的活派给了 writer」。建议用 AgentSpecLoader 写单测兜底。

坑 3:子 Agent 不会自动继承父级所有工具。 我们最初以为 Toolkit 注册的工具全团队默认可用,实际必须在子 Agent 声明的 tools 列表里按名引用 。writer 最初没配 tools,让它「存草稿」时它只能干瞪眼输出文本。权限最小化是设计使然,但第一次很容易踩。

八、下一篇预告

团队跑起来后,新的问题是持久与恢复 :会话能不能断点续传?上下文太长怎么办?记忆怎么分层沉淀?P4 我们把 JsonFileAgentStateStore 换成 Redis,聊聊 AgentScope-Java 的工程化底盘------会话持久化、上下文压缩与分层记忆。


仓库地址:github.com/525128633/a... 系列文章:P1 上手实测 · P2 工具调用实战 · P3 本篇


相关推荐
imDwAaY1 小时前
微信运动每天晚上 10 点推送排行榜,技术上到底有多难?
后端
ServBay1 小时前
PHP 突爆 9.2 分高危漏洞,你升级了吗?
后端·php
武子康2 小时前
撤回发生在重建途中,旧索引还能切回来吗
人工智能·后端·agent
码事漫谈2 小时前
双十一,我为什么想买一台编码显示器
后端
喵个咪3 小时前
GoWind Kanban 拆解:看板拖拽背后——LexoRank、乐观锁与尽力而为广播
后端·架构·go
喵个咪3 小时前
GoWind OA 拆解:用 3000 行 Go 写一个够用的审批工作流引擎
后端·架构·go
喵个咪3 小时前
GoWind Ledger 拆解:单式记账的余额一致性——confirm、补偿,以及没进事务的那些缝隙
后端·架构·go
喵个咪3 小时前
GoWind Shop 拆解:订单超时关单——一个事务里的四联动
后端·架构·go
我命由我123453 小时前
Python 开发 - Python 虚拟环境
运维·开发语言·后端·python·http·https·java-ee