这一步做两件事:把任务卡送进去,把稿子取回来。听起来比第三章的 3.4.2 简单,实际多了一道工序——取回来之后要当场验收。原因很实在:它取回来的不一定是任务卡要的那一份(角度可能跑了、形态可能错了),而后者才是真正贵的错误。
5.3.1 出任务卡 → 5.3.2 把任务卡变成初稿(在智能体那一侧)→ 本节负责两端之间的搬运:把卡送进去、把稿取出来,并顺手跑一遍验收。之后交给 5.2 的第三格【补人味】(人做),再进 5.4.2 排版。本节是 Agent 侧与 RPA 侧的交界处。
一、RPA 在这里扮演什么角色
一个搬运工。 它不生成内容、不改内容,只做三件事:
① 把任务卡原文贴进对话输入框 → 发送
↓
② 等生成结束(等什么?见第四节)
↓
③ 把最后一段返回读出来 → 验收 → 写回任务表
为什么必须由 RPA 来做,而不是人手动复制? 因为这一步是纯机械动作(贴一段文字、等、读一段文字),而且它每篇都要重做一遍。这正是 1.4.2 讲的 RPA 适用条件:规则明确、高频、不含量判断。
如果平台提供了 API(扣子、文心都提供),优先走 API——它比界面稳定得多(不受改版影响、不用等页面渲染)。 但本节的写法按【界面操作】描述,有两个理由:① 界面的做法在所有平台上通用,而 API 各家的鉴权与格式都不一样;② 本课的取稿场景恰好是【对话】——在很多平台上,用界面把话说完、把结果拿走,比调 API 更简单。 判断原则不变:能用 API 就用 API(更稳),只能用界面就界面(更通用)。
二、发起会话:把任务卡【原样】送进去
这一步只有一个要求,但很容易违反:不要顺手改写。
✗ 错的:把任务卡【理解一下】,用自己的话重新组织再发
✓ 对的:把 5.3.1 产出的任务卡原文,一字不改地贴进去
为什么不能改? 因为任务卡里的每一个字段都是对标算出来的结论: - 【切入角度:成本预算】——这是 5.3.1 从缺口里挑出来的,你顺手改一个词,可能就改回了那个挤了 3 篇的红海角度; - 【必须包含至少 3 处具体数字】——这是为了过人味检查(5.3.2 六项里的第三项); - 【形态:回答】——这个字段决定下游 5.4.3 走哪套操作(三路形态互不通用)。
一句话:任务卡是【上游的结论】,不是【给模型的提示词草稿】。 你改它,等于把上游所有工序的产出作废。
三、取稿的三个难点
难点一:稿子在最后一段,不在第一段
多轮对话的返回是一整段文本(可能有开场白、可能有追问、可能有多次修改的痕迹)。要取的字段在最后那一轮里——所以解析要从结尾往上找,不能从头往下找。
难点二:格式会飘
即便提示词里明确要求【只输出 JSON】,实际返回也常常夹着解释文字(比如开头一句【好的,我按成本预算这个角度写了一版:】,结尾一句【需要我再补充吗?】)。
所以解析要分两档做:
1. 先试直接解析(纯 JSON,最理想);
2. 失败就用正则兜底(从第一个 { 取到最后一个 },再试一次);
3. 两档都失败才算失败——不能一失败就报错,那样会把大量【其实能救】的返回丢掉。
这套两档解析与 3.4.2 完全一致,是这一课的固定做法。
难点三(本章新增):取到的不一定是【你要的那一份】
这是本节与 3.4.2 最大的不同,也是本章多出来的那道工序。
模型可能交给你一份格式完全正确、内容也很像样的稿子——但它的形态写成了【文章】(任务卡要的是【回答】)、或者角度写成了【原理科普】(任务卡要的是【成本预算】)。
为什么这比格式错误更危险? 因为格式错误你能一眼看出来;【内容好但方向错】你看不出来。 它会被当成合格稿一路往下走,一直走到发布前,你才发现【这不是我要的角度】——那时候前面几步全白做了。
所以验收必须比对着任务卡做,不能只查格式:
| 查什么 | 比对着谁 |
|---|---|
| 格式对不对(JSON 能解析、字段齐) | 结构 |
| 形态对不对 | 任务卡 |
| 角度对不对 | 任务卡 |
| 字数在不在区间 | 任务卡 |
| 人味够不够 | 5.3.2 的六项 |
注意最后一行:人味六项不需要等到人工检查时才用——接在取稿后面,它就成了一道【自动质检】。这是把 5.3.2 接进流水线的位置。
四、动手 Demo:取稿验收器
它做两关:第一关解析(这段返回里到底有没有一份能用的稿子),第二关验收(取到的这份是不是任务卡要的那一份)。
它要处理四种真实返回:
| 编号 | 返回长什么样 | 正确的处理 |
|---|---|---|
| R1 | 干净的 JSON,角度对,人味够 | 放行 |
| R2 | JSON 前后夹着解释文字 | 兜底解析,但内容太短、人味不够 → 打回重写 |
| R3 | 格式没问题,但形态和角度都跑偏了 | 打回(这是最该警惕的一种) |
| R4 | 结构破损(缺右括号) | 解析失败 → 重发请求(不是打回重写) |
# -*- coding: utf-8 -*-
"""5.4.1 Demo:取稿验收器 —— 从多轮对话的最后一段里【取出稿子,并当场验收】(纯标准库)"""
import json
import re
import statistics
# 任务卡(5.3.1 产出、5.3.2 写进提示词的那张卡)
TASK = {"form": "回答", "angle": "成本预算", "words_min": 300, "words_max": 600}
# 模拟四种子窗口返回(都在【多轮对话的最后一段】里)
REPLIES = {
"R1": '{"form":"回答","qurl":"https://www.zhihu.com/question/123456","angle":"成本预算",'
'"body":"先说预算,我给三档,都是我实际花过的数。第一档二百元以内:窄边换鞋凳一百二,'
'壁挂钥匙盘三十,折叠收纳箱五十,加起来两百,我家玄关就这一档。第二档五百元左右:'
'把换鞋凳换成一米二宽的鞋柜,多一个抽屉放鞋油和伞,约四百,再配一个可视收纳箱八十。'
'第三档一千五左右:做嵌入式玄关柜,柜体一千二,加装感应灯带三百。说实话我觉得没必要上第三档。'
'我家门后只有二十厘米,一体式根本塞不进去,问过两家定制都说做不了。'
'真要花钱,我建议先花二十块买个卷尺,量准三个尺寸:门后到墙、玄关进深、开门半径。'
'量完你会发现,很多方案自己就排除了。我一开始就是没量,买了个三十五厘米的鞋凳,开门撞脚,退了。'
'后来我把这三档记在手机备忘录里,买一样划一笔,基本不会超。",'
'"topics":["收纳","家居"],"ai_assisted":true}',
"R2": '好的,我按成本预算这个角度写了一版:\n'
'{"form":"回答","qurl":"https://www.zhihu.com/question/123456","angle":"成本预算",'
'"body":"收纳预算因人而异,需要根据实际情况确定。合理的预算规划能够帮助您更高效地完成收纳改造。'
'建议先明确需求,再选择适合的工具,最后根据预算范围做出决策。这样可以避免不必要的浪费。",'
'"topics":["收纳"],"ai_assisted":true}\n'
'需要我再补充一些具体价位吗?',
"R3": '{"form":"文章","qurl":"","angle":"原理科普",'
'"body":"家庭收纳是一项需要系统规划的工作。合理的收纳能够显著提升居住体验。'
'首先,我们需要明确收纳的基本原则。分类是收纳的基础。合理的分类能够让物品各归其位。'
'其次,我们要选择适合的收纳工具。收纳盒是常见的选择。不同尺寸的收纳盒适用于不同场景。",'
'"topics":["收纳"],"ai_assisted":true}',
"R4": '{"form":"回答","qurl":"https://www.zhihu.com/question/123456","angle":"成本预算",'
'"body":"这一段我写到一半被打断了,字段也没闭合。","topics":["收纳"',
}
# ---- 解析 ----
def parse(raw):
"""三档:direct / fallback / fail"""
try:
return json.loads(raw), "direct"
except Exception:
pass
m = re.search(r"\{.*\}", raw, re.S)
if m:
try:
return json.loads(m.group(0)), "fallback"
except Exception:
pass
return None, "fail"
# ---- 人味快检(沿用 5.3.2 的六项) ----
CONNECT = ["首先", "其次", "再次", "最后", "综上所述", "总而言之", "总的来说", "由此可见"]
STANCE = ["我觉得", "我认为", "我建议", "我不建议", "说实话", "坦白说", "我个人", "我劝", "我的经验是"]
HESITATE = ["可能", "大概", "不一定", "看情况", "因人而异", "说不准", "未必", "再说吧", "就算了"]
SELFACT = [r"我家", r"我(?!们)[^,。;:!?\n]{0,6}?(?:买|试|换|退|装|用|踩|花|攒|量|清|扔|留|做|选|测|查|问)"]
DETAIL = [r"[\d一二三四五六七八九十百千万两半几]+\s*(?:厘米|毫米|米|寸|公分|平|元|块|天|分钟|小时|秒|个月|年|双|个|张|件)"]
def human_score(text):
n = len(re.sub(r"\s+", "", text)) or 1
ss = [len(p) for p in re.split(r"[。!?!?\n]", text) if len(p.strip()) > 1]
sd = statistics.pstdev(ss) if len(ss) > 1 else 0
hits = [
sd >= 8,
sum(text.count(w) for w in CONNECT) * 100.0 / n <= 1.0,
sum(len(re.findall(p, text)) for p in DETAIL) >= 3,
sum(text.count(w) for w in STANCE) >= 2,
sum(text.count(w) for w in HESITATE) >= 1,
sum(len(re.findall(p, text)) for p in SELFACT) >= 2,
]
return sum(hits), 6
# ---- 验收 ----
print("=" * 74)
print("一、解析(取稿的第一道关:这段返回里到底有没有一份能用的稿子)")
print("=" * 74)
parsed = {}
for k, raw in REPLIES.items():
d, how = parse(raw)
parsed[k] = d
tag = {"direct": "纯 JSON,直接解析 ✔", "fallback": "夹了解释文字,正则兜底 ✔", "fail": "结构破损,解析失败 ✘"}[how]
print(" [%s] %s" % (k, tag))
print()
print("=" * 74)
print("二、验收(取到了稿子,还得问它是不是【任务卡要的那一份】)")
print("=" * 74)
verdicts = {}
for k, d in parsed.items():
if d is None:
print()
print(" [%s] 解析失败 —— 不验收,直接重发请求(并记录一次失败)" % k)
verdicts[k] = "重发"
continue
bad, warn = [], []
if d.get("form") != TASK["form"]:
bad.append("形态是【%s】,任务卡要的是【%s】" % (d.get("form"), TASK["form"]))
if d.get("form") == "回答" and not d.get("qurl"):
bad.append("形态是回答却没有问题链接")
if d.get("angle") != TASK["angle"]:
bad.append("角度跑偏:写成【%s】,任务卡要的是【%s】" % (d.get("angle"), TASK["angle"]))
body = d.get("body", "")
wn = len(re.sub(r"\s+", "", body))
if wn < TASK["words_min"]:
bad.append("正文 %d 字,低于下限 %d" % (wn, TASK["words_min"]))
elif wn > TASK["words_max"]:
warn.append("正文 %d 字,超过上限 %d(提示)" % (wn, TASK["words_max"]))
hs, ht = human_score(body)
if hs < 4:
bad.append("人味仅 %d/%d —— 这一条要回 5.3.2 重写" % (hs, ht))
elif hs < 6:
warn.append("人味 %d/%d —— 能进下一步,但记得在【补人味】那一格补足" % (hs, ht))
print()
print(" [%s] 形态=%s 角度=%s 正文=%d 字 人味=%d/%d"
% (k, d.get("form"), d.get("angle"), wn, hs, ht))
for b in bad:
print(" ✗ %s" % b)
for w in warn:
print(" ! %s" % w)
if bad:
verdicts[k] = "打回"
else:
verdicts[k] = "放行"
print()
print("=" * 74)
print("三、汇总")
print("=" * 74)
for k in REPLIES:
print(" [%s] → %s" % (k, verdicts[k]))
go = [k for k, v in verdicts.items() if v == "放行"]
print()
print(" 放行进【补人味】的:%s" % ("、".join(go) if go else "(无)"))
实际运行输出(真跑,不是示意):
==========================================================================
一、解析(取稿的第一道关:这段返回里到底有没有一份能用的稿子)
==========================================================================
[R1] 纯 JSON,直接解析 ✔
[R2] 夹了解释文字,正则兜底 ✔
[R3] 纯 JSON,直接解析 ✔
[R4] 结构破损,解析失败 ✘
==========================================================================
二、验收(取到了稿子,还得问它是不是【任务卡要的那一份】)
==========================================================================
[R1] 形态=回答 角度=成本预算 正文=314 字 人味=5/6
! 人味 5/6 —— 能进下一步,但记得在【补人味】那一格补足
[R2] 形态=回答 角度=成本预算 正文=86 字 人味=1/6
✗ 正文 86 字,低于下限 300
✗ 人味仅 1/6 —— 这一条要回 5.3.2 重写
[R3] 形态=文章 角度=原理科普 正文=116 字 人味=0/6
✗ 形态是【文章】,任务卡要的是【回答】
✗ 角度跑偏:写成【原理科普】,任务卡要的是【成本预算】
✗ 正文 116 字,低于下限 300
✗ 人味仅 0/6 —— 这一条要回 5.3.2 重写
[R4] 解析失败 —— 不验收,直接重发请求(并记录一次失败)
==========================================================================
三、汇总
==========================================================================
[R1] → 放行
[R2] → 打回
[R3] → 打回
[R4] → 重发
放行进【补人味】的:R1
读结果,五个值得注意的地方:
-
R4的处理方式是本节最容易被做错的一处:解析失败要【重发】,不是【打回重写】。 这两件事看起来都是【不行,重来】,成本差得很远:重发只是把请求再送一次(稿子可能只是生成到一半断了、或者页面刷新了);打回重写则是让模型从头再写一遍——如果原文其实没问题、只是没传完,这一下就白花一次生成。所以【解析失败】必须单独一档。 -
R3才是本章最该警惕的一种失败——它的格式完全正确,措辞也正常,内容甚至挺像样。问题在:形态写成了【文章】(要的是回答)、角度写成了【原理科普】(要的是成本预算)。 如果没有【角度核对】这一条,它会一路往下走——一直走到发布前才发现【这不是我要的角度】,那时候 5.2 任务表后面的几格全白填了。 这就是本节反复强调的:验收必须比对着任务卡做,不能只查格式。 -
R2演示了什么是【格式能救、内容不能救】:兜底解析把它救回来了(前面的解释文字被正确忽略),但它本身太短(86 字)、人味只有 1/6——所以救回来之后照样打回。 这一条与 4.4.2 那句结论完全一致:兜底只负责【让它能被读】,不负责【让它能过】。 -
人味检查在取稿这一步就能算出来,而且
R1的人味是 5/6——只差一项就满分,脚本给的是【提示】而不是【打回】。 这个分档很有用:5/6 的稿子不用重写,人在【补人味】那一格顺手补一项就行(5.3.2 说过:补内容的成本很低)。 如果把它设成【必须 6/6 才放行】,会让大量其实够用的稿子被退回重生成——那是把【严格】用成了【浪费】。 -
四份返回里只有一份放行——这不是脚本太严,这是真实比例。 一次生成的合格率本来就不高(尤其当提示词里带了那么多约束)。而这恰好说明了【验收】这道关存在的意义:它把不合格的挡在【最便宜】的位置上。 一次重发、一次重写的成本,远低于【走到发布前才发现不对】。
两节的解析逻辑是同一套(直接解析 → 正则兜底 → 失败),本节多出来的是第二关【验收】。为什么第三章不需要? 因为第三章的任务很简单(一篇文章、一个主题),模型没什么可跑偏的;而本章的任务卡是【带角度和缺口约束的】——约束越多,跑偏的可能性越大。 一条通用规律:约束越细的任务,验收就要越严。 因为约束是【上游的结论】,而模型并不知道那个结论有多重要。
五、⚠️ 三条工程提醒
这三条都是【跑起来才会遇到】的问题。
提醒一:等待策略不要写死 sleep
生成时间是不确定的(几百字和两千字差很远、高峰期和深夜差很远)。写死 sleep(30) 有两种后果:太短则取到半截稿,太长则每次白等。
正确做法是【轮询一个结束标志】:
| 可行的标志 | 说明 |
|---|---|
| 页面上【停止生成】按钮消失 | 最直接 |
| 正文区域出现结束标点 | 粗略但好用 |
| 返回文本里出现了 JSON 的右花括号 | 本节的场景最适用(稿子以 } 结尾) |
| 超时上限 | 兜底:超过 N 秒都没结束,判定失败 → 走重发 |
提醒二:会话要隔离,每篇一个新会话
这一条与 4.4.3 的【墨滴会记住上一次的内容】是同一类坑,但后果更严重。
如果上一篇的上下文还在,模型可能:
- 把上一篇的题材混进这一篇;
- 把上一篇的【角度】带过来(这正好制造了 R3 那种跑偏);
- 或者回一句【和刚才那篇一样处理吗】——而你解析不了这句话。
做法:每篇开一个新会话;或者在同一个会话里明确说清【换一个任务,忽略上面的内容】。
提醒三:失败的原始返回一定要存下来
因为失败的原因往往不在报错里,在原始返回里。
R4 那种缺右括号,你看解析器的报错只能看到【JSON 解析失败】——但你要知道的是【它到底写完了没有】。存下原文,你才能判断:这是【没生成完】(重发),还是【格式就是错的】(改提示词)。
这条和 5.2 的【要留痕】是同一条原则,只不过5.2 留的是【业务痕迹】(哪篇走到哪一步),这里留的是【工程痕迹】(失败时原文长什么样)。
30 秒实验
把 Demo 里 R3 的 angle 从 原理科普 改成 成本预算,再跑一次。
会发生什么:
R3的【角度跑偏】那条消失,阻断从 4 条降到 3 条——但它仍然是【打回】。(还有形态错、字数不足、人味 0/6)- 顺手再把它
form改成回答并且把body换成R1那段——它就会变成放行。 - 这时候你手上就有了两份【完全相同】的稿子,一份来自
R1、一份来自R3。 它们会被下游一视同仁地处理。
结论:验收器只检查【字段】,不检查【这条路是否正当】。 一份跑偏后被人工改回来的稿子,和一份一次写对的稿子,在下游看来没有区别——这正是为什么【验收要卡在取稿这一步】:越早纠正,越接近源头。
常见误区
- 【解析失败就打回重写】——错,这一步很贵。 解析失败要【重发】。 两者的成本差得很远:重发只是把请求再送一次(稿子可能只是生成到一半断了),打回重写是让模型从头再写一遍。所以解析失败必须单独设一档。
- 【只要能解析出 JSON,就算取稿成功】——错,那只过了第一关。 格式正确、内容像样的稿子,形态和角度都可能跑偏(
R3就是),而这一类比格式错误更危险——因为你看不出来,它会一路走到发布前,让前面几格全白做。 - 【任务卡可以理解一下再发,让模型更好懂】——错,一个字都别改。 任务卡里的每个字段都是对标算出来的结论(角度来自缺口、数字要求来自人味检查、形态决定下游操作)。你顺手改一个词,可能就改回了那个挤了 3 篇的红海角度。 任务卡是上游的结论,不是提示词草稿。
- 【等一会儿再读就行,固定 sleep 30 秒】——错,会两头出错。 生成时间不确定:太短取到半截稿(然后就白白走一次【重发】),太长每次白等。 正确做法是轮询一个【结束标志】(停止生成按钮消失、正文出现结束标点、返回里出现
}),并给一个超时上限兜底。 - 【每次都复用一个会话更省事】——错,这是
R3那种跑偏的常见成因。 上下文不隔离,上一篇的题材和角度会串进这一篇——而【角度被带过来】正好就是本节最警惕的那种错误。 每篇开新会话,或者在同一个会话里明确说清【换任务、忽略上面的内容】。
自测三问(点开对答案)
1. **为什么【解析失败】必须和【验收不通过】分开成两档?** 因为**成本完全不同**:**解析失败走【重发】**——只是把请求再送一次(稿子可能只是生成到一半断了、页面刷新了);**验收不通过走【打回重写】**——让模型从头再生成一遍。 **如果把两者都当【重来】,那么一篇其实写好了、只是没传完的稿子,会被白白重写一次。** 所以解析结果要分三档(直接解析/正则兜底/失败),**失败单独走一条路。** 2. **`R3` 那种失败为什么比格式错误更危险?** 因为**它的格式完全正确、措辞也正常、内容甚至挺像样**——**你看不出来它有问题**。它的问题在**形态(文章 vs 回答)和角度(原理科普 vs 成本预算)**,而这两个字段是**来自任务卡的约束**。 **危险在于它会被当成合格稿一路往下走**,一直到发布前才发现【这不是我要的角度】——**那时候 5.2 任务表后面几格全白填了。** 所以**验收必须比对着任务卡做,不能只查格式。** 3. **为什么说【约束越细的任务,验收就要越严】?** 因为**约束是【上游的结论】,而模型并不知道那个结论有多重要**。5.3.1 算出【成本预算这个角度没人写】,这是个**带判断的结论**;模型看到的只是【写一篇关于成本预算的回答】,**它对【为什么必须是这个角度】毫无概念**,所以**一旦它觉得另一个角度更好写,就会自己换掉**(`R3` 就是)。 **对比第三章**:3.4.2 的任务很单纯(一篇文章、一个主题),**没什么可跑偏的,所以不需要第二关验收。** 本章的卡带了角度和缺口,**约束越多,跑偏的可能越大,验收就必须越严。**- ☐ 下一节(5.4.2 RPA使用墨滴平台排版)
相关笔记
- RPA提交多轮对话信息并获取创作结果 —— 同一套解析逻辑的上一版,本节在它后面加了一关验收
- 对标图文Agent工作流 —— 任务卡的来源,本节为什么【一个字都不能改】
- 知乎图文创作智能体 —— 人味六项的来处,本节把它接成了自动质检
- RPA获取Agent 结果 —— 两端之间搬运的通用方法
- 🤖 Agent+RPA实战 · 索引 —— 全课路线图