用 Python 从零搭一个多智能体系统:从 Mock 闭环到真实 LLM 接入

0. 这篇教程解决什么问题

原文用一个旅游规划系统讲解多智能体系统:把“制定旅行计划”拆成多个专家角色,再让它们按顺序协作。

本文在保留原文思路的基础上,做成一个更适合实战落地的版本:

  1. 先写一个完全离线可运行的 Mock 版本,不用 API Key,先验证流程。
  2. 再抽象出统一的 Agent 类和顺序工作流。
  3. 最后给出接入 OpenAI/OpenRouter 兼容接口的改造方式。
  4. 补上原文没有强调的安全边界:API Key、结构化输出、超时、失败处理、幻觉风险。

核心原则:不要一开始就接真实模型。先让多 Agent 流程在本地闭环跑通,再替换模型后端。

1. 多智能体系统到底是什么

多智能体系统(Multi-Agent System, MAS)不是神秘框架。最小可用版本可以理解为:

把一个复杂任务拆成多个角色,每个角色只负责一段清晰职责,然后把它们串起来完成最终目标。

以旅行规划为例,如果只用一个 Agent,它要同时负责:

这会让 prompt 变长、职责混乱,也不方便排查错误。

拆成多个 Agent 后,可以变成:

  1. ResearchAgent:只做目的地研究。
  2. ActivityAgent:只做每日活动安排。
  3. BudgetAgent:只做费用估算。
  4. FinalAgent:只做最终整合。

最简单的协作方式是顺序链:

用户需求 → ResearchAgent → ActivityAgent → BudgetAgent → FinalAgent → 最终行程

这不是最复杂的多 Agent 架构,但它是最容易跑通、最容易调试、最适合初学者的起点。

2. 项目结构

新建目录:

mkdir python-multi-agent-demo
cd python-multi-agent-demo
mkdir src

最终结构:

python-multi-agent-demo/
├── src/
│   ├── mock_agents.py
│   ├── workflow.py
│   └── openai_backend.py
└── .env.example

先不要安装任何第三方依赖。第一版只用 Python 标准库。

3. 第一版:离线 Mock 多 Agent 闭环

先写一个不调用真实模型的版本,验证“角色拆分 + 顺序传递”是否成立。

创建 src/mock_agents.py

from dataclasses import dataclass


@dataclass
class AgentResult:
    agent_name: str
    output: str


@dataclass
class MockAgent:
    name: str
    role: str

    def run(self, task: str) -> AgentResult:
        if self.name == "Research Agent":
            output = (
                "目的地研究:伊斯坦布尔适合亲子游。推荐圣索菲亚大教堂、"
                "托普卡帕宫、伊斯坦布尔水族馆、KidZania 和博斯普鲁斯海峡游船。"
            )
        elif self.name == "Activity Planner Agent":
            output = (
                "活动安排:第 1 天游览历史城区;第 2 天安排水族馆和 KidZania;"
                "第 3 天安排公园、博物馆和集市。"
            )
        elif self.name == "Budget Agent":
            output = (
                "预算估算:4 人 3 天游约 3100-3800 美元,包含机票、住宿、餐饮、"
                "交通、门票和杂费。"
            )
        else:
            output = f"最终整合:\n{task}\n请根据以上信息生成一份简洁行程。"

        return AgentResult(agent_name=self.name, output=output)

再创建 src/workflow.py

from dataclasses import dataclass

from mock_agents import MockAgent


@dataclass
class TravelRequest:
    origin: str
    destination: str
    days: int
    travelers: int
    budget: str
    interests: str

    def to_prompt(self) -> str:
        return (
            f"出发地:{self.origin}\n"
            f"目的地:{self.destination}\n"
            f"天数:{self.days}\n"
            f"人数:{self.travelers}\n"
            f"预算:{self.budget}\n"
            f"偏好:{self.interests}"
        )


def build_agents() -> list[MockAgent]:
    return [
        MockAgent("Research Agent", "研究目的地、景点和旅行提示"),
        MockAgent("Activity Planner Agent", "根据研究结果安排每日活动"),
        MockAgent("Budget Agent", "估算旅行预算并给出省钱建议"),
        MockAgent("Final Travel Assistant", "整合所有结果,输出最终行程"),
    ]


def run_workflow(request: TravelRequest) -> str:
    current_context = request.to_prompt()
    history: list[str] = []

    for agent in build_agents():
        result = agent.run(current_context)
        history.append(f"## {result.agent_name}\n{result.output}")
        current_context = "\n\n".join(history)

    return current_context


if __name__ == "__main__":
    request = TravelRequest(
        origin="Islamabad",
        destination="Istanbul",
        days=3,
        travelers=4,
        budget="$4000",
        interests="kid friendly",
    )
    print(run_workflow(request))

运行:

python3 src/workflow.py

预期输出形态:

## Research Agent
目的地研究:...

## Activity Planner Agent
活动安排:...

## Budget Agent
预算估算:...

## Final Travel Assistant
最终整合:...

如果这一步跑不通,不要急着接 LLM。先修本地流程。

4. 为什么要先做 Mock 版本

Mock 版本能验证三件事:

  1. 工作流是否合理:每个 Agent 的输入输出是否接得上。
  2. 职责边界是否清晰:有没有某个 Agent 什么都想做。
  3. 最终产物是否可组合:前面 Agent 的结果能不能支撑最后总结。

这一步也是生产系统常见做法:先验证控制流,再替换真实外部依赖。

如果直接接模型,错误来源会混在一起:

Mock 版本可以先排除大部分流程问题。

5. 第二版:把 Agent 后端抽象出来

真实系统里,Agent 不应该只能绑定一个模型。我们可以把“生成文本”的能力抽象为 backend。

创建一个更通用的结构:

from dataclasses import dataclass
from typing import Protocol


class AgentBackend(Protocol):
    def generate(self, system_prompt: str, user_prompt: str) -> str:
        pass


@dataclass
class Agent:
    name: str
    role: str
    backend: AgentBackend

    def run(self, task: str) -> str:
        return self.backend.generate(
            system_prompt=self.role,
            user_prompt=task,
        )

这个结构的好处是:

6. 第三版:接入 OpenAI/OpenRouter 兼容接口

如果要接真实模型,先准备 .env.example

OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY=replace-with-your-key
OPENAI_MODEL=gpt-4.1-mini

注意:

创建 src/openai_backend.py

import os
from dataclasses import dataclass

from openai import OpenAI


@dataclass
class OpenAIBackend:
    base_url: str
    api_key: str
    model: str
    max_tokens: int = 1200

    @classmethod
    def from_env(cls) -> "OpenAIBackend":
        base_url = os.environ.get("OPENAI_BASE_URL", "https://openrouter.ai/api/v1")
        api_key = os.environ["OPENAI_API_KEY"]
        model = os.environ.get("OPENAI_MODEL", "gpt-4.1-mini")
        return cls(base_url=base_url, api_key=api_key, model=model)

    def generate(self, system_prompt: str, user_prompt: str) -> str:
        client = OpenAI(base_url=self.base_url, api_key=self.api_key)
        response = client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
            max_tokens=self.max_tokens,
        )
        content = response.choices[0].message.content
        if not content:
            return ""
        return content

安装依赖:

python3 -m venv .venv
. .venv/bin/activate
pip install openai

运行前设置环境变量:

export OPENAI_BASE_URL="https://openrouter.ai/api/v1"
export OPENAI_API_KEY="你的 API Key"
export OPENAI_MODEL="gpt-4.1-mini"

7. 真实 LLM 版本的工作流写法

将工作流改成注入 backend:

from dataclasses import dataclass

from openai_backend import OpenAIBackend


@dataclass
class Agent:
    name: str
    role: str
    backend: OpenAIBackend

    def run(self, task: str) -> str:
        print(f"{self.name} is working...")
        return self.backend.generate(self.role, task)


def build_agents(backend: OpenAIBackend) -> list[Agent]:
    return [
        Agent(
            "Research Agent",
            "You are an expert travel researcher. Find attractions, hidden gems, local experiences, and travel tips.",
            backend,
        ),
        Agent(
            "Activity Planner Agent",
            "You are a travel activity planner. Create a day-wise plan from the research notes.",
            backend,
        ),
        Agent(
            "Budget Agent",
            "You estimate travel costs. Be concise and clearly mark estimates as estimates.",
            backend,
        ),
        Agent(
            "Final Travel Assistant",
            "You combine research, activities, and budget into a concise final itinerary.",
            backend,
        ),
    ]

关键点:

每个 Agent 的 prompt 越窄,输出越容易检查。

8. 给每一步加结构化输出

原文直接让模型输出自然语言。教学可以,但真实项目最好让每个 Agent 输出结构化内容。

例如 Research Agent 可以要求输出:

{
  "attractions": ["Hagia Sophia", "Topkapi Palace"],
  "local_experiences": ["Bosphorus cruise"],
  "family_friendly_notes": ["KidZania Istanbul is suitable for children"],
  "risks": ["Opening hours and prices need real-time verification"]
}

Budget Agent 可以要求输出:

{
  "currency": "USD",
  "estimated_total_min": 3100,
  "estimated_total_max": 3800,
  "assumptions": ["Flights are estimated, not real-time quotes"],
  "needs_verification": ["visa fees", "hotel prices", "flight prices"]
}

结构化输出的好处:

9. 增加 Validator Agent:让系统更像真实产品

原文的系统少了一个关键角色:校验者。

旅游规划这类任务有很高的幻觉风险,尤其是:

可以增加一个 Validator Agent

Final Travel Assistant → Validator Agent → Final Answer

Validator 的职责不是重写全部内容,而是检查:

  1. 有没有未标注的估算。
  2. 有没有明显过期或无法验证的信息。
  3. 有没有预算加总不一致。
  4. 有没有一天内安排过满。
  5. 哪些内容必须提醒用户实时确认。

Validator Prompt 示例:

You are a strict travel-plan validator.
Check the itinerary for unsupported claims, outdated travel data, budget inconsistencies, and overpacked schedules.
Do not rewrite the itinerary.
Return:
1. blocking_issues
2. warnings
3. facts_that_need_real_time_verification
4. recommended_user_disclaimer

10. 失败处理:不要让 Agent 静默失败

多 Agent 系统最怕的问题是:前面某一步失败了,后面还一本正经继续生成。

建议每个 Agent 返回统一结构:

{
  "ok": true,
  "agent": "Budget Agent",
  "output": "...",
  "warnings": [],
  "error": null
}

失败时:

{
  "ok": false,
  "agent": "Budget Agent",
  "output": null,
  "warnings": [],
  "error": "model timeout"
}

工作流规则:

11. 最小验收标准

一个可交付的多 Agent demo,至少应满足:

12. 常见坑

坑 1:把顺序链误认为复杂多 Agent

原文示例本质是线性流水线。它适合入门,但不等于高级 MAS。

如果你需要真正复杂的协作,可能还需要:

坑 2:让模型凭记忆查实时信息

旅游预算、签证、航班、酒店都不应该只靠模型内部知识。

真实系统应该接:

坑 3:每个 Agent 都写得太宽

如果每个 Agent 都能“自由发挥”,多 Agent 会变成多份重复答案。

好的 Agent prompt 应该窄:

只做预算,不改行程。
只做校验,不重写正文。
只做目的地研究,不生成最终计划。

坑 4:没有最终校验

没有 Validator 的多 Agent 系统,很容易把多个 Agent 的幻觉叠加起来。

多 Agent 不一定降低幻觉;如果没有校验,它可能只是让幻觉更有层次。

13. 推荐的实战升级路径

按这个顺序迭代:

  1. Mock 顺序链:验证流程。
  2. 接真实 LLM:验证角色 prompt。
  3. 改结构化输出:降低整合难度。
  4. 增加 Validator:减少明显错误。
  5. 接实时工具/API:减少事实幻觉。
  6. 加状态和日志:支持复盘与调试。
  7. 再考虑 LangGraph、CrewAI、AutoGen 等框架。

不要反过来。一开始就上框架,通常会把问题藏得更深。

14. 总结

这篇原文的价值不在于它给出了一个生产级多 Agent 系统,而在于它用最小代码解释了多 Agent 的基本思想:

把复杂任务拆成多个角色,让每个角色只处理自己擅长的一段,再把结果组合起来。

真正落地时,需要补上四件事:

  1. Mock-first 的可测试流程。
  2. 结构化输出。
  3. 错误处理和超时策略。
  4. 实时工具与 Validator。

如果你只是学习多 Agent,原文的顺序链足够入门。

如果你要做真实产品,请记住:

多 Agent 的核心不是“Agent 数量多”,而是职责边界清晰、数据流可验证、失败路径可控。