跳转到内容

Action 系统与 ActionNode 结构化输出

学习目标

理解 Action 作为原子能力单元的设计,以及 ActionNode 如何通过 JSON schema 约束 LLM 输出结构化数据。


项目实践

Action 基类

Action 是所有行动的基础类,封装了 LLM 调用、上下文管理和输出格式化:

class Action(SerializationMixin, ContextMixin, BaseModel):
name: str = "" # action 名称
i_context: Union[dict, str, None] = "" # 输入上下文
prefix: str = "" # system message 前缀
desc: str = "" # 描述(用于 skill manager)
node: ActionNode = Field(default=None, exclude=True) # 结构化输出节点
llm_name_or_type: Optional[str] = None # 使用的模型

核心方法:

async def _aask(self, prompt: str, system_msgs=None) -> str:
"""Append default prefix"""
return await self.llm.aask(prompt, system_msgs)
async def run(self, *args, **kwargs):
"""Run action"""
if self.node:
return await self._run_action_node(*args, **kwargs)
raise NotImplementedError("The run method should be implemented in a subclass.")

ActionNode 结构化输出

ActionNode 是 MetaGPT 的核心创新之一,用于约束 LLM 输出为结构化格式:

# ActionNode 定义(简化版)
class ActionNode:
key: str # 字段名
expected_type: Type # 预期类型(str, int, list 等)
instruction: str # 对该字段的指令说明
example: str # 示例值

fill() 方法通过 LLM 填充字段:

async def fill(self, req: str, llm: BaseLLM, schema: str = None):
# 构建 prompt,包含 instruction、example 和 schema 定义
# 调用 LLM 生成符合 JSON schema 的输出
# 解析并验证输出,返回 ActionOutput
return ActionOutput(content=raw_output, instruct_content=structured_obj)

实战:WritePRD 的 ActionNode

WritePRD 为例,其 WRITE_PRD_NODE 包含多个字段:

字段类型说明
Project Namestr项目名称
Product Goalslist[str]产品目标
User Storieslist[str]用户故事
Competitive Analysisstr竞品分析
Competitive Quadrant ChartstrMermaid 竞争象限图
Implementation Approachstr实现方案
UX Planstr用户体验计划

生成 PRD 时,LLM 必须按这些字段输出结构化 JSON,而非自由文本。

从 instruction 自动生成 ActionNode

Action 支持通过 instruction 字符串自动生成 ActionNode

# 模型验证器
@model_validator(mode="before")
def _init_with_instruction(cls, values):
if "instruction" in values:
name = values["name"]
i = values.pop("instruction")
values["node"] = ActionNode(key=name, expected_type=str, instruction=i, example="", schema="raw")
return values

这意味着只需提供一个 instruction 字符串,就可以创建一个能生成结构化输出的 Action。

ActionOutput

ActionOutput 包装 Action 的结构化输出:

class ActionOutput:
content: str # 原始文本
instruct_content: BaseModel # 结构化的 Pydantic 对象

_act() 中,输出会被包装为 AIMessage

response = await self.rc.todo.run(self.rc.history)
if isinstance(response, (ActionOutput, ActionNode)):
msg = AIMessage(
content=response.content,
instruct_content=response.instruct_content,
cause_by=self.rc.todo,
sent_from=self,
)

问题与规避

ActionNode 字段过多导致输出不完整

  • 当字段数量过多(>10),LLM 可能遗漏部分字段或生成截断 JSON
  • 对策:拆分为多个 ActionNode,或使用 exclude 参数在 fill() 时排除可选字段

JSON 解析失败

  • LLM 生成的 JSON 可能包含语法错误(如未转义的引号)
  • 对策:ActionNode.fill() 内置重试机制,解析失败时重新生成
  • 极端情况:使用 CodeParser.parse_code() 从 markdown 代码块中提取 JSON

设计取舍

ActionNode vs OpenAI function calling

  • ActionNode 使用 prompt engineering 实现结构化输出,兼容所有 LLM
  • OpenAI function calling 使用 API 原生能力,但只适用于特定提供商
  • MetaGPT 选择 ActionNode 作为通用方案,function calling 作为 Provider 层优化

结构化 vs 自由文本

  • 结构化输出便于下游处理(如 PRD JSON → 文件系统)
  • 代价是 prompt 更长、token 消耗更多
  • 权衡:核心文档(PRD、Design)使用结构化,简单回复使用自由文本

参考来源

  • 源码验证: metagpt/actions/action.py:29 — Action 基类
  • 源码验证: metagpt/actions/action_node.py — ActionNode 类与 fill() 方法
  • 源码验证: metagpt/actions/write_prd_an.py — WRITE_PRD_NODE 字段定义
  • 源码验证: metagpt/actions/action_output.py — ActionOutput 结构