长文一次写不好,所以要和 Agent 分几轮聊——但【聊】这件事对 RPA 很不友好:它会跑偏、会忘、会在回答外面加一句【好的,以下是】。 本节的全部内容,就是把【聊天】变成【一问一答一记录的协议】,并在最后一轮里准确地拆出三个字段。
3.4.1 送出了第一句话并记下了会话标识,本节接着追着这段对话继续。它是本章技术上最麻烦的一节——麻烦不在于让 AI 写文章,而在于让【对话】变成【可以被程序读的结果】。 这与第二章 2.3.1 那句【输给机器读的东西,宁可多一步也要让它确定】是同一条原则,只是这次的对手是【多轮】。
一、为什么要多轮:三个理由
第一个理由最容易想到,后两个才是关键:
| 理由 | 说明 |
|---|---|
| ① 一次写不好 | 一千多字的长文,一次性生成的质量不可控:结构容易乱、后半段容易疲软 |
| ② 中间产物可以检查 | 提纲先出来,人可以在便宜的阶段拦下来——改提纲比改全文便宜得多 |
| ③ 修改有上下文 | 第三轮说【第二节太啰嗦】,模型知道【第二节】指什么;重新开一段对话,它不知道 |
第 ② 条是本节最值钱的地方:多轮的真正价值不是【质量更高】,而是【把一个黑箱拆成几个可以检查的环节】。 这也正是 3.2 那张链路图里为什么画了【人审提纲】这个关卡。
二、轮次协议:三轮
不要随便聊。 给对话定一个固定的轮次协议,RPA 才可能自动化:
| 轮 | 谁开口 | 说什么 | 期望拿回什么 |
|---|---|---|---|
| 第 1 轮 | RPA | 四个字段(3.4.1 拼的那段) | 提纲:3 个小标题 + 每节一句话 |
| 第 2 轮 | 人/规则 | 【可以,写全文】 | Markdown 全文 + 摘要 |
| 第 3 轮 | 人 | 修改意见(可空) | 修改后的全文 |
| (可选) | 人 | 再改 | …… |
三条纪律:
- 每一轮都要能被机器判定【说完了没有】。 第 1 轮的判据是【出现了 3 个小标题】;第 2 轮的判据是【出现了摘要行】。判据不出现就继续等,等超时了才报异常;
- 轮次要有上限。 建议 3~5 轮封顶——轮数越多,上下文越长,成本越高,而且模型会开始【漂移】(越改越偏离最初的要求);
- 修改意见必须是【人写的结构化短句】,不要让人在对话里自由发挥。RPA 要能把这句话原样贴进去,它不负责理解人话。
这是最容易犯的错:流程跑到一半【弹出对话框让人输入修改意见】——看起来很方便,实际会让整条流程断掉(人不在电脑前怎么办?打了半小时怎么办?)。
正确做法:修改意见写在表里的 note 列,RPA 从表里读。 这样人可以在任何时候修改、随时暂停恢复,流程本身不需要等人。
三、会话状态怎么维护
这是本节最实操的一节。在界面自动化里,你能抓住的会话状态只有两种:
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 一口气跑完 | 3.4.1 提交后不关页面,直接在同一段对话里追问 3 轮 | 简单、不用存状态 | 中间不能长时间停顿;页面一崩全废 |
| 存对话链接 | 每轮结束后把【这段对话的 URL】写回表里的 conversation_ref |
可以停、可以恢复、可以分段跑 | 需要多一步【打开链接 → 等页面加载完】的动作 |
推荐【存对话链接】。 理由与 3.2 一致:内容生产流程天然要人介入(提纲要人看、全文要人改),流程一定会在中间停下。如果状态只存在浏览器标签页里,你一关电脑就没了。
配套的两条防呆:
- 每次续聊前先确认【打开的是这一段对话】——通过页面上的标题或首条消息校验一次,不要盲目相信链接;
- 每轮结束后立刻回写
conversation_ref与当前轮次——不要攒到最后一起写,中途崩了你就不知道跑到第几轮了。
四、从哪一轮取结果
答案:取最后一轮。
但这句话有个前提:你得知道哪一轮是【最后一轮】。 所以表里还需要两个字段:
| 字段 | 含义 |
|---|---|
conversation_ref |
这段对话在哪 |
round |
当前进行到第几轮 |
last_reply |
最后一轮的完整返回原文(必须原样保存) |
因为解析会失败。当解析失败时,你需要两样东西才能修:原始返回长什么样、你的解析规则错在哪。 只保存解析结果、不保存原文,是一个非常常见、也非常昂贵的错误——等到发现结果不对,原始返回已经找不回来了。这一条与第二章 2.4.2 的纪律相同:原文留档,是排查的最后一根绳子。
五、解析:三个字段怎么拆
Agent 的返回是一大段 Markdown,要拆出 title、digest、body_md 三个字段。拆法有三条规则(下面每条都对应一个真实的坑):
规则一:第一个标题行是 title,并且要从正文里删掉
为什么必须删掉:公众号的标题是一个独立字段,不是正文的一部分。 如果正文里还留着 ## 标题,发布后会出现双标题(后台标题栏一个、正文开头一个)——这是公众号排版里最常见的事故之一。
3.3.1 的指令里已经写了输出格式,但模型依然可能不写标题、或用错级别(# 写成 ###)。所以解析器不能只认 ##——凡是行首的 #(一到三级)都当标题候选,取第一个。这就是【规则要宽容,结果要严格】:解析要宽容地抓住它,校验要严格地检查它。
规则二:摘要行要【去掉星号后再判断】
模型很容易写成 **摘要**:xxx。如果你只匹配 ^摘要:,这一行就漏了。 稳妥的做法是:先把这一行的 ** 全删掉,再看它是不是以【摘要】开头。
规则三:要区分【致命缺失】与【可补救缺失】
这是本节最重要的一个判断:
| 缺失 | 严重程度 | 处理 |
|---|---|---|
| 缺标题 | 致命 | 整行标【异常】,交给人(标题是必填字段,且必须由人重写) |
| 缺摘要 | 可补救 | 记进备注,流程继续(摘要可以在公众号后台手工补) |
为什么区别对待:因为收尾的成本不一样。缺草稿就废了整篇;缺摘要只是少了一段文字,后台补 30 秒的事。把可补救的问题也当成致命问题,会让流程动不动就停下来——停下来太频繁的自动化,人最后就不用了。
不是所有错误都值得停下。 判据是:这个错误能不能在后续环节用低成本补上? 能补的→记录并继续;不能补的→立刻停下并交给人。 第二章 2.3.1 的【字段缺 → 给默认值并记备注】,本章的【缺摘要不阻断、缺标题阻断】,讲的是同一件事,只是这里把它说清楚了。
六、动手 Demo:把四种返回都解析一遍
下面这段代码可以直接跑。它把 Agent 四种真实可能出现的返回都过了一遍(标准、夹开场白、忘写摘要、忘写标题),看解析器怎么分别处置。
# -*- coding: utf-8 -*-
"""3.4.2 Demo:从多轮对话的最后一轮里解析出 title / digest / body_md"""
import re
RAW_1 = """## 玄关收纳:我家最后只留下三件东西
玄关永远堆着鞋和快递盒,出门前翻三分钟找不到钥匙。
### 一、先做减法,再谈收纳
我把不穿的鞋和攒了没用的袋子清掉,剩下的空间比想象的大。
摘要:把玄关收拾成只剩三件东西的清爽空间,先减后加,量好尺寸再买。
"""
RAW_2 = """好的,以下是您要的公众号文章:
## 玄关收纳:我家最后只留下三件东西
(正文……)
**摘要**:把玄关收拾成只剩三件东西的清爽空间。
"""
RAW_3 = """## 玄关收纳:我家最后只留下三件东西
(正文……)
(模型忘了写摘要)
"""
RAW_4 = """这是一篇很长的正文,开头就是正文,模型忘了写标题。
"""
def parse(raw):
problems = []
lines = raw.strip().split("\n")
# 0) 剥掉开场白
first = lines[0].replace("**", "").strip()
if re.match(r"^(好的|当然|以下是|这是)", first) and not first.startswith("#"):
lines = lines[1:]
problems.append("已剥离开场白")
# 1) 抽标题:第一个 # 行,并从正文里移除
title, t_idx = None, None
for i, ln in enumerate(lines):
if ln.lstrip().startswith("#"):
title = ln.lstrip().lstrip("#").replace("**", "").strip()
t_idx = i
break
if title is None:
return {"问题": ["致命:未找到标题行"]} # 致命 → 整行异常
if len(title) > 64:
problems.append("标题超 64 字(当前 %d 字)" % len(title))
body_lines = lines[:t_idx] + lines[t_idx + 1:]
# 2) 抽摘要:先去掉星号,再以「摘要」开头且含冒号
digest, d_idx = "", None
for i, ln in enumerate(body_lines):
c = ln.replace("**", "").strip()
if c.startswith("摘要") and (":" in c or ":" in c):
digest = re.split(r"[::]", c, maxsplit=1)[1].strip()
d_idx = i
break
if d_idx is None:
problems.append("缺摘要(不阻断,可在后台补)") # 可补救 → 继续
else:
body_lines = body_lines[:d_idx] + body_lines[d_idx + 1:]
if len(digest) > 120:
problems.append("摘要超 120 字(当前 %d 字)" % len(digest))
body = re.sub(r"\n{3,}", "\n\n", "\n".join(body_lines)).strip()
return {"title": title, "digest": digest, "body_md": body, "问题": problems}
实际运行输出(真跑,不是示意):
============================================================
第 1 轮返回(标准)
============================================================
title = 玄关收纳:我家最后只留下三件东西
digest = 把玄关收拾成只剩三件东西的清爽空间,先减后加,量好尺寸再买。
body_md 长度 = 119 字
正文首行 = 玄关永远堆着鞋和快递盒,出门前翻三分钟找不到钥匙。
备注 = (无)
============================================================
第 2 轮返回(夹开场白+加粗摘要)
============================================================
title = 玄关收纳:我家最后只留下三件东西
digest = 把玄关收拾成只剩三件东西的清爽空间。
body_md 长度 = 61 字
正文首行 = 玄关永远堆着鞋和快递盒,出门前翻三分钟找不到钥匙。
备注 = 已剥离开场白
============================================================
第 3 轮返回(忘写摘要)
============================================================
title = 玄关收纳:我家最后只留下三件东西
digest = (空)
body_md 长度 = 72 字
正文首行 = 玄关永远堆着鞋和快递盒,出门前翻三分钟找不到钥匙。
备注 = 缺摘要(不阻断,可在公众号后台手工补)
============================================================
第 4 轮返回(忘写标题)
============================================================
[异常] 致命:未找到标题行(模型没写 ## 标题) → 状态改【异常】,整行交给人
读结果,四个值得注意的地方:
- 第 2 轮里,摘要是写成
**摘要**:…的——如果解析器只匹配^摘要:,这一行就会漏掉。去掉星号再判断这个小动作,挡掉了一整类失败; - 第 2 轮的
body_md只有 61 字(比第 1 轮短)——因为两段正文我写得不一样,这不表示解析错了。看长度只能发现【异常的大或异常的小】,判断内容对不对必须靠人。 这也说明为什么解析器不能替人做内容判断; - 第 3 轮摘要为空,但流程继续——备注里写了原因,人在后台补一句话就行;
- 第 4 轮整行异常——没有标题,这篇稿子根本没法进下一步(公众号标题是必填),所以立刻停下交给人,而不是猜一个标题。
七、三种失败与兜底
| 失败 | 表现 | 兜底 |
|---|---|---|
| 解析失败 | 返回里没有可识别的字段结构 | 原文存进 last_reply,状态改【异常】,绝不猜 |
| 缺关键字段 | 缺标题 | 同上(致命) |
| 缺非关键字段 | 缺摘要 | 备注记录,流程继续 |
| 内容越界 | 出现禁用词 | 标记出来交给人(不要自动改,理由见下) |
| 轮数超限 | 已聊到第 5 轮还没收敛 | 停下,把已聊到的最好一版存下来交给人 |
第二章 2.3.1 提到过【第一件】被误判成排名承诺的例子。自动替换会掩盖问题:改完表面上一切正常,但语义可能已经坏了(【最好】改成【较好】,整句话的意思就变了)。 正确做法:标出来,交给人。 这与【缺摘要继续跑】不矛盾——缺摘要是【缺了一块】,可以补;违禁词是【埋了一颗雷】,必须人来拆。
八、⚙️ 2026 现状:多轮对话的三个现实问题
- 上下文长度与成本:轮数越多,每轮携带的历史越长,调用成本上升、响应变慢,且模型会开始【漂移】。所以轮次要封顶(建议 3~5 轮),并且不要把所有历史都带上——只带【提纲 + 当前全文 + 本轮意见】往往就够了;
- 不同平台的会话机制不一样:自定义 GPT 的会话状态绑在网页上(本节讲的都是这个前提);扣子这类平台可以用变量显式传递,会话状态反而更好管理。如果换成扣子,本节第三节的【存对话链接】可以直接换成【存变量】;
- 【对话正在退役】这个词不夸张:3.3.1 讲过的 GPT 退役里,历史对话不迁移。所以重要的成品稿一定要落到自己的表里(本章的
body_md列就是这个作用)——不要把【成果】留在对话记录里。
30 秒实验
看这段返回,说说解析器会怎么处置:
以下是修改后的全文:
## 玄关收纳:三件东西就够了
(正文……)
小结:先减后加,量好尺寸再买。
答案:标题和正文能拿到,但摘要会被判定为【缺失】,流程继续、备注记录。
原因:模型把【摘要】写成了【小结】。解析器只认【摘要】两个字。
两条结论:
- 模型永远会以你没预料到的方式换词——所以要么把解析规则放宽(同时认【摘要/小结/导语】),要么在指令里把这两个字说死(3.3.1 的指令里写了【用一句话给出【摘要】】,就是为这里做准备);
- 这就是为什么笔记里要留
last_reply——你能看到它到底写了什么,才知道该放宽哪条规则。
常见误区
- 【多轮太麻烦,一次写完就行】——错。 多轮的真正价值不只是质量,而是【把一个黑箱拆成几个可以检查的环节】。提纲先出来,人能在便宜的阶段拦下来。
- 【流程跑到一半弹窗让人输入修改意见】——错。 这会让流程必须等人,人不在就断。修改意见写在表的
note列,RPA 从表里读——人可以随时改、随时暂停恢复。 - 【解析结果对了就行,原始返回不用存】——错。 解析失败时你需要两样东西才能修:原文长什么样、规则错在哪。不存原文,等发现问题时它已经找不回来了。
- 【模型没写标题,就让解析器从正文第一句里截一个当标题】——错。 标题是必填字段、且必须由人来定(它决定打开率,是公众号里最重要的一个字段)。缺标题就停下,不要猜。
- 【发现违禁词就自动替换掉】——错。 自动替换会掩盖问题(改完看着正常,语义可能已经坏了)。违禁词要标出来交给人——它是雷,不是缺口。
自测三问(点开对答案)
1. **多轮对话的真正价值是什么?为什么修改意见要写在表里而不是弹窗让人输入?** 真正价值是**把一次性生成的黑箱,拆成几个【可以检查、可以拦截】的环节**——提纲先出来,人可以在便宜的阶段改。修改意见写在表的 `note` 列而不弹窗,是因为**弹窗会让流程必须等人**:人不在电脑前、或者想了半小时,整条流程就断了。**从表里读,人能随时改、流程能随时恢复。** 2. **为什么【缺标题】要阻断、而【缺摘要】只记录?这个判据叫什么?** 因为**两者在后续环节的补救成本完全不同**:标题是必填字段且决定打开率,缺了整篇稿子没法进下一步;**摘要只是少一段文字,在公众号后台 30 秒就能补上**。判据叫**分级阻断**——**能低成本补上的,记录并继续;不能补的,立刻停下交给人。** 不适用分级阻断的自动化,会因为动不动就停下而被人弃用。 3. **解析器为什么要【去掉星号后再判断摘要行】,并且要从正文里【删掉标题行】?** **去星号**是因为模型很容易写成 `**摘要**:…`,只匹配 `^摘要:` 会漏掉一整类格式。**删标题行**是因为公众号的标题是一个**独立字段**,正文里如果再留一个 `## 标题`,**发布后会出现双标题**(后台标题栏一个、正文开头一个),这是公众号排版最常见的事故之一。两件事都体现同一条原则:**解析要宽容地抓住它,校验要严格地检查它。**- ☐ 下一节(3.4.3 排版 GPT 创作结果)
相关笔记
- RPA提交创作信息 —— 本节要用的会话标识从哪来
- RPA获取创作结果 —— 上一章的解析,对照看单轮与多轮的差别
- RPA获取Agent 结果 —— 取结果的通用方式
- 排版 GPT 创作结果 —— 本节拿到的 Markdown 稿,下一节要转成公众号能吃的 HTML
- 🤖 Agent+RPA实战 · 索引 —— 全课路线图