19_项目实战五_多Agent协作写日报_Supervisor调度Researcher_Writer_Reviewer

概述

很多人第一次做多 Agent 协作,会写成这样:

text 复制代码
Researcher Agent -> Writer Agent -> Reviewer Agent -> 最终日报

看起来很清楚,但真实项目里会出现一堆细节问题。

  • Researcher 查到的任务数据不完整,Writer 却开始自由发挥。
  • Writer 写得很像总结,但缺少工时、阻塞和明日计划。
  • Reviewer 发现问题后,只给一句"需要修改",没有结构化修改意见。
  • 多个 Agent 共享完整上下文,token 越滚越大。
  • 每个 Agent 都能调用所有工具,职责边界混乱。
  • Supervisor 不知道什么时候该重试、什么时候该终止。
  • 最终日报格式不稳定,无法推送到飞书、钉钉或邮件模板。

所以,多 Agent 协作不是把几个 LLM 串起来聊天,而是要给每个 Agent 明确职责、输入、输出和边界,再由 Supervisor 统一调度。

本文会实现一个"多 Agent 协作写日报"项目:

  • Researcher Agent:从 Jira/Tapd/本地任务系统拉取当天任务和提交记录。
  • Writer Agent:根据结构化事实生成日报草稿。
  • Reviewer Agent:检查日报是否缺项、夸大、格式错误或与事实不一致。
  • Supervisor Agent:负责调度三个子 Agent,决定是否重写、是否结束。
  • FastAPI:提供 /daily-report 接口。
  • Pydantic:固定日报、审查意见和最终响应结构。
  • LangSmith:观察每个 Agent 的工具调用和调度过程。

多 Agent 的价值不是"人多力量大",而是通过职责隔离和上下文隔离使各自职责边界清晰,让复杂任务更可控。

项目目标:自动生成一份能直接发出去的日报

我们希望用户这样调用:

text 复制代码
用户:帮我生成 2026-07-01 的研发日报,团队是 payment-backend。

系统内部执行:

text 复制代码
1. Supervisor 接收任务。
2. 调用 Researcher 拉取 Jira/Tapd 任务、Git 提交、阻塞事项。
3. 调用 Writer 生成日报草稿。
4. 调用 Reviewer 检查草稿质量。
5. 如果 Reviewer 要求修改,Supervisor 把修改意见交回 Writer。
6. 最终输出格式统一、事实准确、可直接发送的日报。

最终结果类似:

markdown 复制代码
## 研发日报:payment-backend(2026-07-01)

### 今日完成

- 完成退款回调幂等校验,已合并到 `main`。
- 修复订单状态同步失败问题,新增异常重试日志。
- 补充退款接口单元测试 6 条。

### 进行中

- 支付渠道对账任务重构,当前完成数据模型设计,预计明日联调。

### 风险与阻塞

- Tapd-2034 依赖风控接口字段确认,已在群内同步,等待风控同学回复。

### 明日计划

- 完成对账任务联调。
- 补充渠道异常场景测试。
- 跟进风控字段确认结果。

这份日报不能只追求"文笔好",更重要的是:

  • 内容来自真实任务数据。
  • 不编造成果。
  • 风险和阻塞不能遗漏。
  • 格式稳定。
  • 可以进入团队日报系统。

#mermaid-svg-2UltJl7FbXfJn0w5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2UltJl7FbXfJn0w5 .error-icon{fill:#552222;}#mermaid-svg-2UltJl7FbXfJn0w5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2UltJl7FbXfJn0w5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2UltJl7FbXfJn0w5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2UltJl7FbXfJn0w5 .marker.cross{stroke:#333333;}#mermaid-svg-2UltJl7FbXfJn0w5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2UltJl7FbXfJn0w5 p{margin:0;}#mermaid-svg-2UltJl7FbXfJn0w5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2UltJl7FbXfJn0w5 .cluster-label text{fill:#333;}#mermaid-svg-2UltJl7FbXfJn0w5 .cluster-label span{color:#333;}#mermaid-svg-2UltJl7FbXfJn0w5 .cluster-label span p{background-color:transparent;}#mermaid-svg-2UltJl7FbXfJn0w5 .label text,#mermaid-svg-2UltJl7FbXfJn0w5 span{fill:#333;color:#333;}#mermaid-svg-2UltJl7FbXfJn0w5 .node rect,#mermaid-svg-2UltJl7FbXfJn0w5 .node circle,#mermaid-svg-2UltJl7FbXfJn0w5 .node ellipse,#mermaid-svg-2UltJl7FbXfJn0w5 .node polygon,#mermaid-svg-2UltJl7FbXfJn0w5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2UltJl7FbXfJn0w5 .rough-node .label text,#mermaid-svg-2UltJl7FbXfJn0w5 .node .label text,#mermaid-svg-2UltJl7FbXfJn0w5 .image-shape .label,#mermaid-svg-2UltJl7FbXfJn0w5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2UltJl7FbXfJn0w5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2UltJl7FbXfJn0w5 .rough-node .label,#mermaid-svg-2UltJl7FbXfJn0w5 .node .label,#mermaid-svg-2UltJl7FbXfJn0w5 .image-shape .label,#mermaid-svg-2UltJl7FbXfJn0w5 .icon-shape .label{text-align:center;}#mermaid-svg-2UltJl7FbXfJn0w5 .node.clickable{cursor:pointer;}#mermaid-svg-2UltJl7FbXfJn0w5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2UltJl7FbXfJn0w5 .arrowheadPath{fill:#333333;}#mermaid-svg-2UltJl7FbXfJn0w5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2UltJl7FbXfJn0w5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2UltJl7FbXfJn0w5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2UltJl7FbXfJn0w5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2UltJl7FbXfJn0w5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2UltJl7FbXfJn0w5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2UltJl7FbXfJn0w5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2UltJl7FbXfJn0w5 .cluster text{fill:#333;}#mermaid-svg-2UltJl7FbXfJn0w5 .cluster span{color:#333;}#mermaid-svg-2UltJl7FbXfJn0w5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2UltJl7FbXfJn0w5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2UltJl7FbXfJn0w5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2UltJl7FbXfJn0w5 .icon-shape,#mermaid-svg-2UltJl7FbXfJn0w5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2UltJl7FbXfJn0w5 .icon-shape p,#mermaid-svg-2UltJl7FbXfJn0w5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2UltJl7FbXfJn0w5 .icon-shape .label rect,#mermaid-svg-2UltJl7FbXfJn0w5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2UltJl7FbXfJn0w5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2UltJl7FbXfJn0w5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2UltJl7FbXfJn0w5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
需要修改
用户请求
Supervisor Agent
Researcher Agent
任务事实包
Writer Agent
日报草稿
Reviewer Agent
是否通过
最终日报

日报生成的目标不是"像日报",而是基于事实生成可发送、可审计、可复用的工作报告。

技术选型:用 Supervisor 模式做集中调度

LangChain 当前多 Agent 设计里,常见模式包括 Subagents、Handoffs、Skills、Router 和自定义工作流。

本文使用的是 Supervisor / Subagents 模式:

text 复制代码
主 Agent = Supervisor
子 Agent = Researcher / Writer / Reviewer
子 Agent 被包装成 Supervisor 可调用的工具

这种方式有几个好处:

  • 调度权在 Supervisor 手里。
  • 子 Agent 不直接和用户交互。
  • 每个子 Agent 有自己的 Prompt 和工具。
  • 子 Agent 的上下文可以隔离,避免主上下文膨胀。
  • Supervisor 可以根据 Reviewer 结果决定是否重试。

技术栈如下:

能力 选型 说明
主调度 create_agent() 构建 Supervisor Agent
子 Agent create_agent() Researcher、Writer、Reviewer
工具定义 @tool 把子 Agent 包装成工具
模型初始化 init_chat_model() 统一模型入口
结构化输出 Pydantic 固定事实包、草稿、审查结果
状态保存 Checkpointer 支持同一天报告重试和追踪
API 服务 FastAPI 对外提供日报生成接口
观测 LangSmith 追踪跨 Agent 调用链

安装依赖:

bash 复制代码
pip install -U langchain langchain-openai langgraph pydantic fastapi uvicorn

环境变量:

bash 复制代码
export OPENAI_API_KEY="sk-..."
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."

Windows PowerShell:

powershell 复制代码
$env:OPENAI_API_KEY="sk-..."
$env:LANGSMITH_TRACING="true"
$env:LANGSMITH_API_KEY="..."

项目结构:

text 复制代码
daily_report_agents/
  api.py
  models.py
  data_sources.py
  researcher_agent.py
  writer_agent.py
  reviewer_agent.py
  supervisor_agent.py
  render.py

说明:本文用本地 mock 数据模拟 Jira/Tapd/Git。真实项目只需要把 data_sources.py 换成企业内部 API 即可。

Supervisor 模式适合"流程可控、角色明确、最终由一个入口对用户负责"的多 Agent 项目。

数据模型:先定义 Agent 之间传递什么

多 Agent 系统最怕"靠自然语言传自然语言"。

Researcher 给 Writer 一段散文,Writer 再给 Reviewer 一段散文,最后 Supervisor 很难判断哪里出错。

所以我们先定义结构。

创建 models.py

python 复制代码
from typing import Literal

from pydantic import BaseModel, Field


TaskStatus = Literal["done", "in_progress", "blocked", "todo"]
ReviewDecision = Literal["approved", "revise"]


class WorkItem(BaseModel):
    source: str = Field(description="数据来源,例如 jira、tapd、git")
    key: str = Field(description="任务或提交编号")
    title: str
    status: TaskStatus
    owner: str
    evidence: str = Field(description="能证明该事项存在的链接、提交号或任务状态")
    notes: str = ""


class DailyFacts(BaseModel):
    date: str
    team: str
    member: str | None = None
    completed: list[WorkItem] = Field(default_factory=list)
    in_progress: list[WorkItem] = Field(default_factory=list)
    blocked: list[WorkItem] = Field(default_factory=list)
    tomorrow_candidates: list[WorkItem] = Field(default_factory=list)
    raw_summary: str = ""


class DailyDraft(BaseModel):
    title: str
    completed: list[str] = Field(default_factory=list)
    in_progress: list[str] = Field(default_factory=list)
    blockers: list[str] = Field(default_factory=list)
    tomorrow_plan: list[str] = Field(default_factory=list)
    markdown: str


class ReviewIssue(BaseModel):
    severity: Literal["high", "medium", "low"]
    field: str = Field(description="问题所在栏目,例如 completed、blockers、tomorrow_plan")
    message: str
    suggestion: str


class ReviewResult(BaseModel):
    decision: ReviewDecision
    issues: list[ReviewIssue] = Field(default_factory=list)
    revised_instructions: str = ""


class FinalReport(BaseModel):
    date: str
    team: str
    decision: Literal["completed", "failed"]
    markdown: str
    facts: DailyFacts | None = None
    review: ReviewResult | None = None
    warnings: list[str] = Field(default_factory=list)

这里有四类核心对象:

对象 谁产生 谁消费
DailyFacts Researcher Writer、Reviewer、Supervisor
DailyDraft Writer Reviewer、Supervisor
ReviewResult Reviewer Supervisor、Writer
FinalReport Supervisor API、前端、日报系统

这个结构会让每个 Agent 的责任非常明确:

  • Researcher 只负责事实,不写日报。
  • Writer 只根据事实写草稿,不自己查数据。
  • Reviewer 只检查草稿,不重新创作。
  • Supervisor 只调度和收口,不做底层查询。

多 Agent 协作要先设计数据契约,再设计 Prompt。

数据源:模拟 Jira、Tapd 和 Git 提交

真实项目里,Researcher 会连接 Jira、Tapd、GitLab、GitHub、禅道或内部工时系统。

为了让示例可运行,我们先用本地 mock 数据。

创建 data_sources.py

python 复制代码
from models import WorkItem


def fetch_jira_tasks(team: str, date: str) -> list[WorkItem]:
    return [
        WorkItem(
            source="jira",
            key="PAY-1024",
            title="退款回调增加幂等校验",
            status="done",
            owner="yang",
            evidence="Jira PAY-1024 status=Done",
            notes="已处理重复回调导致状态反复更新的问题",
        ),
        WorkItem(
            source="jira",
            key="PAY-1033",
            title="支付渠道对账任务重构",
            status="in_progress",
            owner="yang",
            evidence="Jira PAY-1033 status=In Progress",
            notes="已完成数据模型设计,明日进入联调",
        ),
        WorkItem(
            source="jira",
            key="PAY-1045",
            title="确认风控接口新增字段",
            status="blocked",
            owner="yang",
            evidence="Jira PAY-1045 status=Blocked",
            notes="等待风控同学确认 risk_tag 字段含义",
        ),
    ]


def fetch_tapd_tasks(team: str, date: str) -> list[WorkItem]:
    return [
        WorkItem(
            source="tapd",
            key="Tapd-2034",
            title="订单状态同步失败补偿",
            status="done",
            owner="yang",
            evidence="Tapd-2034 closed_at=2026-07-01",
            notes="新增异常重试日志和失败告警",
        )
    ]


def fetch_git_commits(team: str, date: str) -> list[WorkItem]:
    return [
        WorkItem(
            source="git",
            key="8f4a91c",
            title="add idempotency check for refund callback",
            status="done",
            owner="yang",
            evidence="commit 8f4a91c",
            notes="修改 app/services/refund_callback.py",
        ),
        WorkItem(
            source="git",
            key="91c0e22",
            title="add refund api unit tests",
            status="done",
            owner="yang",
            evidence="commit 91c0e22",
            notes="新增 6 条退款接口单元测试",
        ),
    ]

真实接入时建议注意:

  • Jira/Tapd 任务状态要映射成统一枚举。
  • Git 提交要按作者、团队、日期过滤。
  • 不要只依赖 commit message,最好关联 PR、任务号和文件变更。
  • 数据源失败时,要把失败信息写进 warnings,不要让模型编造。

Researcher 的质量上限取决于数据源质量,不取决于 Prompt 写得多漂亮。

Researcher Agent:只负责收集事实

Researcher 的任务是把多个数据源整理成 DailyFacts

它不应该写完整日报,也不应该美化措辞。

创建 researcher_agent.py

python 复制代码
import json
from typing import Any

from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool

from data_sources import fetch_git_commits, fetch_jira_tasks, fetch_tapd_tasks
from models import DailyFacts, WorkItem


def _json(data: Any) -> str:
    return json.dumps(data, ensure_ascii=False, default=str)


@tool
def get_jira_tasks(team: str, date: str) -> str:
    """Fetch Jira tasks for a team and date."""
    return _json([item.model_dump() for item in fetch_jira_tasks(team, date)])


@tool
def get_tapd_tasks(team: str, date: str) -> str:
    """Fetch Tapd tasks for a team and date."""
    return _json([item.model_dump() for item in fetch_tapd_tasks(team, date)])


@tool
def get_git_commits(team: str, date: str) -> str:
    """Fetch Git commits for a team and date."""
    return _json([item.model_dump() for item in fetch_git_commits(team, date)])


SYSTEM_PROMPT = """
你是 Researcher Agent,负责收集日报事实。

规则:
1. 你只收集和归纳事实,不写最终日报。
2. 必须调用 Jira、Tapd、Git 三类工具。
3. 将任务按 done、in_progress、blocked、todo 分类。
4. 不要编造工具结果里不存在的任务。
5. 如果多个来源指向同一件事,要合并表达,但保留 evidence。
6. 输出必须是 DailyFacts 结构。
"""


def build_researcher_agent():
    model = init_chat_model("openai:gpt-5.4-mini", temperature=0)

    return create_agent(
        model=model,
        tools=[get_jira_tasks, get_tapd_tasks, get_git_commits],
        system_prompt=SYSTEM_PROMPT,
        response_format=DailyFacts,
    )


researcher_agent = build_researcher_agent()


def research_daily_facts(team: str, date: str, member: str | None = None) -> DailyFacts:
    message = f"请收集日报事实:team={team}, date={date}, member={member or 'all'}"
    result = researcher_agent.invoke(
        {"messages": [{"role": "user", "content": message}]}
    )
    return result["structured_response"]

一个典型 DailyFacts 会长这样:

json 复制代码
{
  "date": "2026-07-01",
  "team": "payment-backend",
  "completed": [
    {
      "source": "jira",
      "key": "PAY-1024",
      "title": "退款回调增加幂等校验",
      "status": "done",
      "owner": "yang",
      "evidence": "Jira PAY-1024 status=Done",
      "notes": "已处理重复回调导致状态反复更新的问题"
    }
  ],
  "blocked": [
    {
      "source": "jira",
      "key": "PAY-1045",
      "title": "确认风控接口新增字段",
      "status": "blocked",
      "owner": "yang",
      "evidence": "Jira PAY-1045 status=Blocked",
      "notes": "等待风控同学确认 risk_tag 字段含义"
    }
  ]
}

Researcher 要输出可追溯事实包,而不是一段看起来像事实的文字。

Writer Agent:根据事实写日报草稿

Writer 的任务是把 DailyFacts 转成日报草稿。

它可以调整语言,但不能新增事实。

创建 writer_agent.py

python 复制代码
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model

from models import DailyDraft, DailyFacts


SYSTEM_PROMPT = """
你是 Writer Agent,负责根据 DailyFacts 写研发日报草稿。

写作规则:
1. 只能使用 DailyFacts 中提供的事实,不要编造成果。
2. 日报必须包含:今日完成、进行中、风险与阻塞、明日计划。
3. 每条内容要具体,避免"优化系统""修复问题"这种空话。
4. 如果存在 blocked 项,必须写入风险与阻塞。
5. 明日计划应该来自 in_progress、blocked 和 tomorrow_candidates。
6. 输出必须是 DailyDraft 结构。
7. markdown 字段必须是可直接发送的 Markdown。
"""


def build_writer_agent():
    model = init_chat_model("openai:gpt-5.4-mini", temperature=0.2)

    return create_agent(
        model=model,
        tools=[],
        system_prompt=SYSTEM_PROMPT,
        response_format=DailyDraft,
    )


writer_agent = build_writer_agent()


def write_daily_draft(facts: DailyFacts, extra_instructions: str = "") -> DailyDraft:
    message = (
        "请根据下面的 DailyFacts 写日报草稿。\n\n"
        f"DailyFacts:\n{facts.model_dump_json(indent=2, ensure_ascii=False)}\n\n"
        f"额外修改要求:{extra_instructions or '无'}"
    )

    result = writer_agent.invoke(
        {"messages": [{"role": "user", "content": message}]}
    )
    return result["structured_response"]

Writer 的 Prompt 里最重要的是这句:

text 复制代码
只能使用 DailyFacts 中提供的事实,不要编造成果。

如果不加限制,模型很容易把"进行中"写成"已完成",或者为了让日报看起来更饱满,补一些不存在的"优化性能""提升稳定性"。

日报是工作记录,不是宣传稿。

Writer 可以润色表达,但不能扩大事实边界。

Reviewer Agent:检查事实、格式和缺项

Reviewer 负责挑问题。

它要检查:

  • 是否有必填栏目缺失。
  • 是否把未完成任务写成已完成。
  • 是否遗漏阻塞项。
  • 是否出现 DailyFacts 里没有的成果。
  • 明日计划是否和进行中/阻塞事项对应。
  • Markdown 是否能直接发送。

创建 reviewer_agent.py

python 复制代码
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model

from models import DailyDraft, DailyFacts, ReviewResult


SYSTEM_PROMPT = """
你是 Reviewer Agent,负责审查研发日报草稿。

审查规则:
1. 对照 DailyFacts 检查日报,发现事实不一致必须指出。
2. 如果 DailyFacts 中有 blocked 项,但日报没有风险与阻塞,必须要求修改。
3. 如果 markdown 缺少今日完成、进行中、风险与阻塞、明日计划任一栏目,必须要求修改。
4. 如果日报新增了 DailyFacts 中不存在的成果,必须要求修改。
5. 如果只是措辞不够优美,不要要求修改。
6. 只有在事实准确、栏目完整、格式可发送时,decision 才能是 approved。
7. 输出必须是 ReviewResult 结构。
"""


def build_reviewer_agent():
    model = init_chat_model("openai:gpt-5.4-mini", temperature=0)

    return create_agent(
        model=model,
        tools=[],
        system_prompt=SYSTEM_PROMPT,
        response_format=ReviewResult,
    )


reviewer_agent = build_reviewer_agent()


def review_daily_draft(facts: DailyFacts, draft: DailyDraft) -> ReviewResult:
    message = (
        "请审查这份日报草稿。\n\n"
        f"DailyFacts:\n{facts.model_dump_json(indent=2, ensure_ascii=False)}\n\n"
        f"DailyDraft:\n{draft.model_dump_json(indent=2, ensure_ascii=False)}"
    )

    result = reviewer_agent.invoke(
        {"messages": [{"role": "user", "content": message}]}
    )
    return result["structured_response"]

Reviewer 的输出示例:

json 复制代码
{
  "decision": "revise",
  "issues": [
    {
      "severity": "high",
      "field": "blockers",
      "message": "DailyFacts 中存在 PAY-1045 阻塞项,但日报未写入风险与阻塞。",
      "suggestion": "在风险与阻塞中补充风控接口 risk_tag 字段等待确认。"
    }
  ],
  "revised_instructions": "请补充风险与阻塞栏目,并确保明日计划包含跟进风控字段确认。"
}

Reviewer 不负责写得更好看,它负责防止日报不真实、不完整、不可发送。

Supervisor Agent:把子 Agent 包装成工具

现在我们把三个子 Agent 包成 Supervisor 能调用的工具。

创建 supervisor_agent.py

python 复制代码
import json
from typing import Any

from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool

from models import DailyDraft, DailyFacts, FinalReport, ReviewResult
from researcher_agent import research_daily_facts
from reviewer_agent import review_daily_draft
from writer_agent import write_daily_draft


def _json(data: Any) -> str:
    return json.dumps(data, ensure_ascii=False, default=str)


@tool
def call_researcher(team: str, date: str, member: str | None = None) -> str:
    """Call Researcher Agent to collect structured daily facts."""
    facts = research_daily_facts(team=team, date=date, member=member)
    return facts.model_dump_json(ensure_ascii=False)


@tool
def call_writer(facts_json: str, extra_instructions: str = "") -> str:
    """Call Writer Agent to generate a daily report draft from DailyFacts JSON."""
    facts = DailyFacts.model_validate_json(facts_json)
    draft = write_daily_draft(facts, extra_instructions=extra_instructions)
    return draft.model_dump_json(ensure_ascii=False)


@tool
def call_reviewer(facts_json: str, draft_json: str) -> str:
    """Call Reviewer Agent to check a daily report draft against DailyFacts."""
    facts = DailyFacts.model_validate_json(facts_json)
    draft = DailyDraft.model_validate_json(draft_json)
    review = review_daily_draft(facts, draft)
    return review.model_dump_json(ensure_ascii=False)


SYSTEM_PROMPT = """
你是 Supervisor Agent,负责调度 Researcher、Writer、Reviewer 生成日报。

工作流程:
1. 必须先调用 call_researcher 获取 DailyFacts。
2. 再调用 call_writer 生成 DailyDraft。
3. 再调用 call_reviewer 审查 DailyDraft。
4. 如果 reviewer 返回 revise,你最多允许再调用 call_writer 一次,并把 revised_instructions 传给 Writer。
5. 修改后必须再次调用 call_reviewer。
6. 如果最终 reviewer approved,返回 FinalReport(decision="completed")。
7. 如果两轮后仍未 approved,返回 FinalReport(decision="failed"),并在 warnings 中说明问题。
8. 不要绕过任何一个子 Agent。
9. 最终输出必须是 FinalReport 结构。
"""


def build_supervisor_agent():
    model = init_chat_model("openai:gpt-5.4-mini", temperature=0)

    return create_agent(
        model=model,
        tools=[call_researcher, call_writer, call_reviewer],
        system_prompt=SYSTEM_PROMPT,
        response_format=FinalReport,
    )


supervisor_agent = build_supervisor_agent()


def generate_daily_report(team: str, date: str, member: str | None = None) -> FinalReport:
    message = (
        f"请生成研发日报:team={team}, date={date}, member={member or 'all'}。"
        "请严格按 Supervisor 工作流程执行。"
    )
    result = supervisor_agent.invoke(
        {"messages": [{"role": "user", "content": message}]}
    )
    return result["structured_response"]

这就是 Supervisor 模式的核心。

子 Agent 本身是 Agent,但对 Supervisor 来说,它们是工具:

工具 背后是谁 输入 输出
call_researcher Researcher Agent team、date、member DailyFacts JSON
call_writer Writer Agent DailyFacts JSON、修改要求 DailyDraft JSON
call_reviewer Reviewer Agent DailyFacts JSON、DailyDraft JSON ReviewResult JSON

Supervisor 不需要知道每个子 Agent 内部怎么做,只需要知道何时调用、传什么、拿回什么。

为什么不用三个 Agent 自由对话?

有人会问:为什么不直接让三个 Agent 在群聊里讨论?

因为日报生成不是开放式讨论,而是一个有明确交付物的业务流程。

自由对话会带来几个问题:

  • 谁负责最终结论不清楚。
  • 上下文会被无关讨论污染。
  • 很难限制某个 Agent 的工具权限。
  • 很难保证 Writer 一定等待 Researcher 完成。
  • 很难保证 Reviewer 的意见一定被处理。
  • 很难做结构化输出和自动重试。

Supervisor 模式更适合这种流程:

text 复制代码
Researcher 只查事实
Writer 只写草稿
Reviewer 只审查
Supervisor 只调度和收口

这不是为了"模拟一个团队会议",而是为了把多种能力组合成一个可靠流程。

业务流程需要的是可控编排,不是 Agent 群聊。

加上确定性流程:Supervisor 不一定全靠模型决策

上面的 Supervisor Agent 把调度也交给模型。

但在生产环境里,如果流程非常固定,可以用普通 Python 控制流来调度,只把复杂语言任务交给 Agent。

例如:

python 复制代码
from models import FinalReport
from researcher_agent import research_daily_facts
from reviewer_agent import review_daily_draft
from writer_agent import write_daily_draft


def generate_daily_report_deterministic(team: str, date: str, member: str | None = None) -> FinalReport:
    facts = research_daily_facts(team=team, date=date, member=member)
    draft = write_daily_draft(facts)
    review = review_daily_draft(facts, draft)

    if review.decision == "revise":
        draft = write_daily_draft(
            facts,
            extra_instructions=review.revised_instructions,
        )
        review = review_daily_draft(facts, draft)

    if review.decision == "approved":
        return FinalReport(
            date=date,
            team=team,
            decision="completed",
            markdown=draft.markdown,
            facts=facts,
            review=review,
        )

    return FinalReport(
        date=date,
        team=team,
        decision="failed",
        markdown=draft.markdown,
        facts=facts,
        review=review,
        warnings=["日报两轮审查后仍未通过,请人工确认。"],
    )

这个版本没有 Supervisor LLM 调度,但仍然是 Supervisor 架构。

区别在于:

  • Agentic Supervisor:调度更灵活,适合任务路径变化大的场景。
  • Deterministic Supervisor:流程更稳定,适合日报、周报、审批这类固定流程。

本文标题说"Supervisor 调度",不一定意味着每一步调度都必须由 LLM 决定。

能用确定性代码表达的流程,不要强行交给 LLM。

API 接口:把日报生成服务化

创建 api.py

python 复制代码
from fastapi import FastAPI
from pydantic import BaseModel, Field

from supervisor_agent import generate_daily_report


app = FastAPI(title="Daily Report Multi-Agent System")


class DailyReportRequest(BaseModel):
    team: str = Field(min_length=1)
    date: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}$")
    member: str | None = None


class DailyReportResponse(BaseModel):
    date: str
    team: str
    decision: str
    markdown: str
    warnings: list[str] = []


@app.post("/daily-report", response_model=DailyReportResponse)
def create_daily_report(request: DailyReportRequest) -> DailyReportResponse:
    result = generate_daily_report(
        team=request.team,
        date=request.date,
        member=request.member,
    )

    return DailyReportResponse(
        date=result.date,
        team=result.team,
        decision=result.decision,
        markdown=result.markdown,
        warnings=result.warnings,
    )

启动:

bash 复制代码
uvicorn api:app --reload --port 8000

调用:

bash 复制代码
curl -X POST http://127.0.0.1:8000/daily-report \
  -H "Content-Type: application/json" \
  -d '{"team":"payment-backend","date":"2026-07-01","member":"yang"}'

返回:

json 复制代码
{
  "date": "2026-07-01",
  "team": "payment-backend",
  "decision": "completed",
  "markdown": "## 研发日报:payment-backend(2026-07-01)\n\n### 今日完成\n...",
  "warnings": []
}

后续可以很容易接入:

  • 飞书机器人。
  • 钉钉群机器人。
  • 企业微信。
  • 邮件系统。
  • 内部日报平台。
  • 定时任务。

多 Agent 系统最终要变成稳定服务,而不是停留在 notebook demo。

渲染层:团队日报格式不要写死在 Prompt 里

前面的 Writer 直接生成 markdown

如果团队格式很固定,更推荐让 Writer 输出结构化内容,然后由代码渲染 Markdown。

创建 render.py

python 复制代码
from models import DailyDraft


def render_daily_markdown(draft: DailyDraft) -> str:
    lines: list[str] = []

    lines.append(f"## {draft.title}")
    lines.append("")

    lines.append("### 今日完成")
    lines.append("")
    for item in draft.completed or ["无"]:
        lines.append(f"- {item}")
    lines.append("")

    lines.append("### 进行中")
    lines.append("")
    for item in draft.in_progress or ["无"]:
        lines.append(f"- {item}")
    lines.append("")

    lines.append("### 风险与阻塞")
    lines.append("")
    for item in draft.blockers or ["无"]:
        lines.append(f"- {item}")
    lines.append("")

    lines.append("### 明日计划")
    lines.append("")
    for item in draft.tomorrow_plan or ["无"]:
        lines.append(f"- {item}")

    return "\n".join(lines)

这样做的好处:

  • 格式统一。
  • 更容易换模板。
  • 不会因为模型输出 Markdown 少了一个标题导致解析失败。
  • 可以为不同团队提供不同模板。

Writer 只需要负责内容:

json 复制代码
{
  "completed": ["完成退款回调幂等校验"],
  "in_progress": ["支付渠道对账任务重构进行中"],
  "blockers": ["风控接口 risk_tag 字段待确认"],
  "tomorrow_plan": ["完成对账任务联调"]
}

Markdown 由 render_daily_markdown() 生成。

固定格式交给代码,语言生成交给模型。

上下文隔离:每个 Agent 只看自己需要的内容

多 Agent 的核心优势之一是上下文隔离。

在这个项目中:

Agent 需要看到什么 不需要看到什么
Researcher team、date、数据源工具 Writer 草稿、Reviewer 意见
Writer DailyFacts、修改要求 Jira 原始 API 响应、Git 完整 diff
Reviewer DailyFactsDailyDraft 数据源工具、Supervisor 历史调度
Supervisor 子 Agent 结果摘要 每个子 Agent 的完整内部推理过程

这样做有三个好处:

  • 降低 token 成本。
  • 减少无关信息干扰。
  • 更容易定位哪个环节出错。

一个常见错误是把所有东西都塞给每个 Agent:

text 复制代码
Jira 全量数据 + Git 提交 + Writer 草稿 + Reviewer 意见 + 历史日报 + 用户偏好

这会让每个 Agent 都变成"全能 Agent",最终职责不清。

多 Agent 不是复制多个全能助手,而是给每个角色最小必要上下文。

重试策略:Reviewer 不通过时怎么改?

本文设置了最多一轮重写:

text 复制代码
Writer -> Reviewer -> 如果 revise -> Writer -> Reviewer -> 结束

为什么不是无限循环?

因为无限循环会带来成本和稳定性问题。

真实项目里可以这样分级:

Reviewer 结果 处理方式
approved 直接发送
revise 且问题可修复 自动重写一次
revise 且两轮未通过 交给人工
数据源缺失 不让 Writer 编造,返回失败
高风险事实冲突 阻断自动发送

可以给 ReviewIssue 增加 auto_fixable 字段:

python 复制代码
class ReviewIssue(BaseModel):
    severity: str
    field: str
    message: str
    suggestion: str
    auto_fixable: bool = True

当出现不可自动修复的问题,例如 Jira API 失败、Git 数据为空、任务状态冲突,就不要继续重写。

自动重试要有上限,数据问题不要靠写作 Agent 修。

状态保存:同一天日报要能追踪

日报系统通常需要追踪:

  • 谁发起生成。
  • 生成了几次。
  • 每次拿到哪些事实。
  • Writer 生成了什么草稿。
  • Reviewer 提了哪些问题。
  • 最终有没有发送。

如果使用 LangGraph checkpointer,可以给同一天同一团队设置 thread_id

python 复制代码
from langgraph.checkpoint.memory import InMemorySaver


checkpointer = InMemorySaver()

supervisor_agent = create_agent(
    model=model,
    tools=[call_researcher, call_writer, call_reviewer],
    system_prompt=SYSTEM_PROMPT,
    response_format=FinalReport,
    checkpointer=checkpointer,
)

result = supervisor_agent.invoke(
    {"messages": [{"role": "user", "content": message}]},
    config={"configurable": {"thread_id": "daily-report:payment-backend:2026-07-01"}},
)

生产环境不要用内存 checkpointer,而应该换成持久化存储。

同时建议把核心对象落库:

内容
daily_report_runs team、date、status、发起人、耗时
daily_report_facts Researcher 事实包
daily_report_drafts Writer 草稿
daily_report_reviews Reviewer 审查结果
daily_report_outputs 最终 Markdown 和发送状态

可追踪性是生产级 Agent 系统的基本能力,不是锦上添花。

人工确认:日报是否应该自动发送?

日报看起来风险不高,但仍然可能出错:

  • 把未完成任务写成完成。
  • 泄露内部敏感项目名。
  • 漏掉重要阻塞。
  • 发送到错误群。
  • 把个人日报发成团队日报。

所以建议分阶段上线:

阶段 策略
Demo 阶段 只生成,不发送
试运行 生成后人工确认
小团队 Reviewer 通过后自动发到测试群
生产 低风险自动发,高风险人工确认

可以设计一个审批接口:

python 复制代码
@app.post("/daily-report/{run_id}/approve")
def approve_daily_report(run_id: str):
    report = load_report(run_id)
    send_to_feishu(report.markdown)
    mark_sent(run_id)
    return {"ok": True}

如果接入 LangGraph human-in-the-loop,也可以在发送前中断,等待人工审批后继续。

自动生成和自动发送是两件事,后者需要更严格的权限和审批。

常见问题点:多 Agent 项目最容易错在哪里?

1. 每个 Agent 都能调用所有工具

表现:

text 复制代码
Writer 自己去查 Jira,然后和 Researcher 的事实冲突。

解决:

  • Researcher 才能访问数据源工具。
  • Writer 只能看到 DailyFacts
  • Reviewer 只能看到 DailyFactsDailyDraft

2. 子 Agent 输出太随意

表现:

text 复制代码
Researcher 返回"今天完成了不少工作",Writer 无法判断具体是什么。

解决:

  • 每个子 Agent 都使用 Pydantic 结构化输出。
  • Supervisor 只传结构化 JSON。

3. Reviewer 太爱挑措辞

表现:

text 复制代码
Reviewer 一直要求"语言更正式",导致循环重写。

解决:

  • Prompt 明确"只因事实、缺项、格式问题要求修改"。
  • 限制重试次数。

4. Supervisor 过度自由

表现:

text 复制代码
Supervisor 跳过 Reviewer,直接返回日报。

解决:

  • System Prompt 固定流程。
  • 生产场景用确定性 Python 工作流兜底。
  • 在日志中检查每次运行是否调用完整链路。

5. 日报内容越来越像模板废话

表现:

text 复制代码
今日持续推进项目开发,优化系统稳定性,提高代码质量。

解决:

  • Writer 必须引用具体任务标题、提交或阻塞事项。
  • Reviewer 检查是否有空泛表达。
  • 没有事实就输出"无",不要硬写。

多 Agent 最大的问题不是 Agent 不够多,而是职责边界、结构化契约和停止条件不清楚。

生产化 Checklist:上线前至少检查这些

检查项 建议
数据源权限 Jira/Tapd/Git API 使用最小权限 token
数据完整性 数据源失败时明确告警,不允许编造
结构化输出 每个 Agent 都定义 Pydantic schema
职责隔离 每个 Agent 只拿自己需要的工具和上下文
重试上限 Reviewer 不通过最多重写 1-2 次
人工确认 自动发送前支持人工审批
敏感信息 过滤客户名、密钥、内部代号等敏感内容
观测追踪 打开 LangSmith trace,记录子 Agent 调用链
成本控制 限制历史数据范围和日报长度
审计日志 保存事实包、草稿、审查结果和最终输出
模板管理 Markdown 模板由代码或配置控制
反馈闭环 支持用户标记"事实错误""格式问题""可直接发送"

推荐架构:

text 复制代码
定时任务 / 用户请求
        |
Daily Report API
        |
Supervisor
        |
Researcher Agent ---- Jira / Tapd / Git
        |
Writer Agent
        |
Reviewer Agent
        |
FinalReport JSON
        |
人工确认 / 自动发送
        |
飞书 / 钉钉 / 邮件 / 内部日报系统

如果团队规模大,还可以把 Researcher 再拆成多个子 Agent:

  • JiraResearcher
  • GitResearcher
  • IncidentResearcher
  • MeetingResearcher

但不要一开始就拆太细。

多 Agent 的拆分粒度应该来自真实职责边界,而不是为了看起来更智能。

完整流程:一次日报生成怎么跑?

以这个请求为例:

text 复制代码
team=payment-backend
date=2026-07-01
member=yang

完整流程如下:

text 复制代码
1. Supervisor 收到请求。

2. Supervisor 调用 call_researcher。

3. Researcher 调用:
   - get_jira_tasks
   - get_tapd_tasks
   - get_git_commits

4. Researcher 返回 DailyFacts。

5. Supervisor 调用 call_writer。

6. Writer 根据 DailyFacts 生成 DailyDraft。

7. Supervisor 调用 call_reviewer。

8. Reviewer 检查:
   - 是否包含今日完成。
   - 是否包含进行中。
   - 是否包含风险与阻塞。
   - 是否包含明日计划。
   - 是否存在事实不一致。

9. 如果 Reviewer approved:
   Supervisor 返回 FinalReport。

10. 如果 Reviewer revise:
    Supervisor 把 revised_instructions 交给 Writer 重写。

11. 第二轮 Reviewer 仍不通过:
    返回 failed,等待人工处理。

这个流程里,LLM 参与了事实归纳、写作和审查,但流程边界是清楚的。

text 复制代码
事实 -> 草稿 -> 审查 -> 修订 -> 最终输出

多 Agent 协作要让每一步都有可验证产物,而不是只看最后一段话。

总结

本文实现了一个多 Agent 协作写日报系统:

  • Researcher Agent 收集 Jira/Tapd/Git 数据。
  • Writer Agent 根据事实生成日报草稿。
  • Reviewer Agent 审查事实一致性、栏目完整性和格式。
  • Supervisor Agent 统一调度子 Agent,并控制最多一轮重写。
  • FastAPI 对外提供日报生成接口。
  • Pydantic 固定 Agent 之间的数据契约。
  • LangSmith 可用于观察整条调用链。

这个项目的关键不是"用了三个 Agent",而是每个 Agent 都有清楚边界

一个可用的多 Agent 日报系统,本质上是"事实采集 + 草稿生成 + 质量审查 + Supervisor 调度 + 结构化输出 + 人工确认"的工程化流程。