Skip to content

第十阶段:报告生成、SelectorGroupChat 与审核死循环 ​

上一阶段团队只会在终端里打印结论。这一阶段让它画图、写报告、保存文件。下面按实际踩坑的顺序讲。

一、新增 writer,coder 负责画图和存文件 ​

  • 新增 writer(报告撰写员):根据执行结果写 Markdown 报告,并用 [Image 0: 说明] 引用图片。
  • coder 用 matplotlib 画图:
  • 开头写 matplotlib.use('Agg')。Agg 是不弹窗口、只输出文件的后端,适合由程序自动执行的代码。
  • 中文字体设成 Microsoft YaHei,否则中文会显示成方框。
  • 用 plt.savefig 把 png 保存到 output/,不要调用 plt.show()。
  • writer 写好报告后,coder 再写代码把它保存为 output/report.md。

二、第一次:RoundRobin、上限 30,修到一半就停了 ​

审核员很尽责,抓出一个矛盾:报告说"2026-04 是全品类共同低点",但鼠标的销量恰恰在 4 月达到峰值。可是还没修完,消息数就到了 30 条的上限,团队被强制停止。

原因:RoundRobin 是死板的轮流发言。比如只需要 writer 改一句话,也要等 coder、executor 等人依次说完一轮,轮次被白白浪费。

三、第二次:coder 写复杂代码,自己把自己搞崩 ​

这次 coder 自己写了一个通用的表格格式化函数,对每一列都用 :,.0f 格式化,连字符串类型的"月份"列也不例外。结果报错:

ValueError: Unknown format code 'f' for object of type 'str'

脚本报错了,审核员也就不承认 report.md 是有效产出。

修法:在提示词里要求 coder 写简单代码,不要自己写格式化函数,表格直接用 df.to_markdown()。这个方法依赖 tabulate 库:

bash
pip install tabulate

四、换成 SelectorGroupChat ​

SelectorGroupChat 不再固定轮流,而是让模型根据 selector_prompt 和对话记录挑选下一个发言者。比如代码写好了就选 executor,执行报错了就选 coder,只是措辞有问题就直接选 writer。

allow_repeated_speaker=True 允许同一个人连续发言,比如 coder 可以连续修几次 bug。

五、第三次:审核死循环 ​

流程顺畅了,又出现新问题:审核员每一轮都能挑出新的措辞毛病,比如"幅度接近"不够准确、某句话"过于肯定"、"99.99% 四舍五入"的写法不规范……writer 改一处,审核员又挑另一处,一直不给 APPROVE。

原因:提示词没有告诉审核员什么算"必须改",它就把所有能挑的毛病都当成不通过的理由。

修法:

  1. 区分阻塞问题和建议。只有两类问题会阻塞通过:结论里的数字与执行结果不符或自相矛盾;图片或报告文件没有保存成功。措辞类问题只列为建议,不影响通过。
  2. 最多打回 2 次,第 3 次审核只要没有数字错误就 APPROVE。
  3. 从源头减少毛病:要求 writer 避免强断言,推测性的内容写成"可能""需验证"。

六、最终代码与结果 ​

最终版使用 SelectorGroupChat,消息上限是 50 条。10_autogen_report.py:

python
import os, asyncio
from dotenv import load_dotenv
from autogen_agentchat.agents import AssistantAgent, CodeExecutorAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_ext.code_executors.local import LocalCommandLineCodeExecutor

load_dotenv()

# 模型客户端:非 OpenAI 官方的模型要写 model_info,告诉 AutoGen 这个模型具备哪些能力
model = OpenAIChatCompletionClient(
model=os.getenv("LLM_MODEL"),
base_url=os.getenv("LLM_BASE_URL"),
api_key=<redacted>("LLM_API_KEY"),
model_info={"vision": False, "function_calling": True, "json_output": True,
"family": "unknown", "structured_output": False},
)

# 1. 分析师:拆解任务、提出分析思路
analyst = AssistantAgent("analyst", model_client=model, system_message=
"你是数据分析师。根据任务提出清晰的分析步骤,交给 coder 写代码。看到执行结果后给出业务结论。")

# 2. 程序员:只负责写代码
coder = AssistantAgent("coder", model_client=model, system_message=
"你是 Python 程序员。按分析师的要求写 pandas 代码,放在 ```python 代码块里。"
"数据文件是 data/sales.csv(utf-8-sig 编码)。用 print 输出关键数字。"
"需要画图时用 matplotlib,开头加 import matplotlib; matplotlib.use('Agg'),"
"中文字体用 plt.rcParams['font.sans-serif']=['Microsoft YaHei'],"
"图片用 plt.savefig 保存到 output/ 目录(先 os.makedirs('output', exist_ok=True)),不要 plt.show()。"
"当 writer 给出报告全文时,写代码把它原样保存为 output/report.md(utf-8)。"
"代码保持简单:不要自己写通用的表格格式化函数,表格用 df.to_markdown()。")

# 3. 执行器:真的去运行 coder 写的代码,结果交回群聊
executor = CodeExecutorAgent("executor",
code_executor=LocalCommandLineCodeExecutor(work_dir="."))

writer = AssistantAgent("writer", model_client=model, system_message=
"你是报告撰写员。根据执行结果写 Markdown 报告:标题、数据概况、结论(带数字)、"
"用 [Image 1: 说明] 引用 output/ 里的图片、经营建议。"
"图片路径写相对 report.md 的文件名,如 [Image 2: 销量趋势]。"
"只陈述数据能直接证明的事实,避免"决定了""升级为"这类强断言,推测要写"可能""需验证"。")

reviewer = AssistantAgent("reviewer", model_client=model, system_message=
"你是审核员。只检查两类:1)结论里的数字与执行结果不符或自相矛盾;"
"2)图片或 output/report.md 没有成功保存。"
"措辞、语气、四舍五入写法之类属于,列出来即可,不影响通过。"
"没有阻塞问题就必须在最后单独写一行 APPROVE。"
"同一份报告最多打回 2 次,第 3 次审核只要没有数字错误就 APPROVE。")

stop = TextMentionTermination("APPROVE") | MaxMessageTermination(50)

selector_prompt = """你在为数据分析团队选择下一位发言者。成员:
{roles}
规则:analyst 先规划;需要代码时选 coder;coder 写出代码后必须选 executor;
执行报错就选 coder 修复;数据齐全后选 writer 写报告,再选 coder 保存文件、executor 执行;
文件确认保存后选 reviewer;reviewer 指出措辞问题选 writer,指出代码问题选 coder。
对话记录:
{history}
从 {participants} 中只回复一个名字。"""

team = SelectorGroupChat(
[analyst, coder, executor, writer, reviewer],
model_client=model,
selector_prompt=selector_prompt,
termination_condition=stop,
allow_repeated_speaker=True, # 允许同一个人连着说,比如 coder 连续修 bug
)

task = """分析 data/sales.csv(utf-8-sig 编码)。
表结构:列为 月份(字符串, 如 2026-01)、产品、销量、单价、销售额;共 18 行,是"月份×产品"的汇总表。
要求:1)各产品总销售额,画柱状图 output/sales_by_product.png;
2)各产品月度销量趋势,画折线图 output/sales_trend.png;
3)给出经营建议;4)整理成报告保存为 output/report.md。"""

async def main():
await Console(team.run_stream(task = task))

asyncio.run(main())

运行 python 10_autogen_report.py 后,审核员最终输出 APPROVE,output/ 里生成了 sales_by_product.png、sales_trend.png 和 report.md。

七、不要提交生成的文件 ​

output/ 里都是每次运行重新生成的文件,已经加入 .gitignore,不会提交到仓库。

小结 ​

  • 让 Agent 写简单代码,少出错。
  • 不是严格按顺序干活的流程,用 SelectorGroupChat 按需选人,比 RoundRobin 省轮次。
  • 审核员要有明确的通过标准和打回次数上限,否则会陷入死循环。

下一阶段预告 ​

第十一阶段:给 Agent 加上人类审批(UserProxyAgent / human-in-the-loop),关键步骤由你拍板,然后才继续往下执行。

👉 下一阶段:第十一阶段:人类审批(human-in-the-loop)