概述
很多人第一次做多 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 | DailyFacts、DailyDraft |
数据源工具、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 只能看到
DailyFacts和DailyDraft。
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:
JiraResearcherGitResearcherIncidentResearcherMeetingResearcher
但不要一开始就拆太细。
多 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 调度 + 结构化输出 + 人工确认"的工程化流程。