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 Name | str | 项目名称 |
| Product Goals | list[str] | 产品目标 |
| User Stories | list[str] | 用户故事 |
| Competitive Analysis | str | 竞品分析 |
| Competitive Quadrant Chart | str | Mermaid 竞争象限图 |
| Implementation Approach | str | 实现方案 |
| UX Plan | str | 用户体验计划 |
生成 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 结构