把 Macaron-V1 的多能力理解成「每一轮都能正常用」,最容易踩的坑是在同一会话里连续切换差异大的任务:结构化抽取刚跑完,接一段开放写作,再回到多轮追问,输出可能从某一轮开始跑偏。这时很难判断是模型在切换点失稳,还是自己的提示写法变了。比较省事的做法是把握不住的地方交给流程:把任务顺序固定下来,每轮响应单独落盘,让「变差」落到具体轮次上,再用固定提示写法重跑一次做排除。
先在同一个会话里按三组不同任务顺序各跑一遍并保存每轮响应,逐轮记录输出长度、结构是否守约、是否答偏,标出第一处明显下滑;然后换成同一提示模板重跑对比。适用场景是多能力模型在连续切换任务后输出跑偏、又说不清原因;操作动作是把任务顺序和提示写法分开验证;验证方式是三组顺序文件齐全且条数与顺序表对应、重跑前后同一任务逐轮可比;边界是结论只在当前接入参数、会话长度和模型版本下成立,换环境需要重测。
选三类差异明显的任务
切换带来的干扰是否可观察,取决于三类任务之间的差异够不够大。建议选:偏结构化输出的任务、偏开放式写作的任务、偏多轮追问的任务。每类只准备一条固定输入,测试期间不改内容,否则后面无法判断变化来自哪一侧。
偏结构化输出的任务用硬约束判断守约:给定一段固定文本和固定字段,要求只输出 JSON、不解释、不加代码块。这类输出可以直接解析,守约与否不靠主观感觉。
从下面的文本中抽取字段,只输出 JSON,不要解释,不要使用代码块。
字段:{"name": "", "date": "", "amount": ""}
文本:<测试期间固定不变的一段文本>
偏开放式写作的任务用固定题目、固定体裁和固定字数区间,判断标准不是好坏,而是有没有答偏、有没有把上一类任务的格式带进来。偏多轮追问的任务建议固定三轮:第一轮给背景,第二轮追问细节,第三轮要求按前面的约束改写。三轮逐条发送,不要合并成一条,否则测不到上下文漂移。
三类任务的提示原文分别存成 prompts/struct.txt、prompts/write.txt、prompts/followup.txt,后续每次重跑都从这几个文件读取,避免手抄导致的写法漂移。
按不同顺序各跑一遍并保存
只跑一次说明不了问题,偶然性无法排除。建议至少准备三组顺序:结构化→写作→追问、追问→写作→结构化、写作→结构化→追问。每组顺序使用一个新会话,顺序之间不共享上下文,这样「切换」只发生在会话内部。
用顺序表驱动脚本,不要手点。下面的骨架里接入口、字段名、会话参数都是占位,按你的实际接入方式替换即可;重点是三件事——同组顺序复用一个会话标识、每轮响应单独写文件、同时保存原始提示。
# orders.yaml
orders:
- name: order_1
tasks: [struct, write, followup]
- name: order_2
tasks: [followup, write, struct]
- name: order_3
tasks: [write, struct, followup]
tasks:
struct: {prompt: prompts/struct.txt}
write: {prompt: prompts/write.txt}
followup: {prompt: prompts/followup.txt} # 内含 3 轮,逐轮发送
import json, pathlib, time, yaml, requests # HTTP 客户端可替换
CFG = yaml.safe_load(open("orders.yaml", encoding="utf-8"))
PROMPTS = {k: pathlib.Path(v["prompt"]).read_text(encoding="utf-8")
for k, v in CFG["tasks"].items()}
def call_model(session_id, text):
r = requests.post(ENDPOINT, # 换成你的接入地址
headers={"Authorization": "Bearer " + API_KEY},
json={"session_id": session_id, # 同组顺序复用一个会话
"input": text,
"params": {"temperature": 0}}, # 采样参数固定并记录
timeout=120)
r.raise_for_status()
return r.json()["output"]
for order in CFG["orders"]:
d = pathlib.Path("runs") / order["name"]
d.mkdir(parents=True, exist_ok=True)
sid = order["name"] + "-" + str(int(time.time()))
for i, task in enumerate(order["tasks"], start=1):
p = PROMPTS[task]
out = call_model(sid, p)
(d / f"turn{i:02d}_{task}.txt").write_text(out, encoding="utf-8")
(d / f"turn{i:02d}_{task}.meta.json").write_text(json.dumps(
{"order": order["name"], "turn": i, "task": task,
"session_id": sid, "params": {"temperature": 0},
"prompt": p}, ensure_ascii=False, indent=2), encoding="utf-8")
跑完后先做形式校验,不要急着看内容:三组目录都要存在,每个目录里的 .txt 条数与顺序表对应(多轮追问按 3 轮算就是 3 条,总条数要能对上),.meta.json 里的 order、turn、task 与顺序表逐项一致。可以直接用命令数列数:
for d in runs/*/; do printf '%s ' "$d"; ls "$d" | grep -c '\.txt$'; done
条数对不上说明中途请求失败或重试覆盖了文件,这组顺序的结果不能参与对比,需要清空目录重跑。
定位质量开始下滑的那一轮
逐轮登记,不要凭印象说「后面变差了」。建议按下表记录,长度用字数或字符数都行,关键是在同一套统计口径下前后可比。
| 顺序 | 轮次 | 任务 | 输出长度 | 结构是否守约 | 是否答偏 | 第一处明显下滑 |
|---|---|---|---|---|---|---|
| order_1 | 01 | struct | JSON 可解析 / 必需字段齐全 | 否 | ||
| order_1 | 02 | write | 不适用 | 待填 | 待标记 | |
| order_1 | 03 | followup | 不适用 | 待填 | 待标记 |
「结构是否守约」用可执行的检查,不靠肉眼:JSON 能否解析、必需字段是否齐全、有没有出现解释性前言。「是否答偏」按任务定标准:结构化任务看是否混入了写作口吻或无关段落;写作任务看是否跑题、是否复述上一轮的结构化字段;追问任务看第三轮是否丢掉了第一轮给出的约束。
下滑位置的判断规则:如果某个任务在排第 1 轮时正常,换到排第 2、第 3 轮就明显变差,且变差发生在切换点之后的相邻一轮,那么切换干扰的可能性更大;如果同一任务在三组顺序里不管排第几都表现一致地差,那么更可能是提示写法或该任务本身超出可用范围,与切换关系不大。两种情况的处理方向不同,所以一定要三组顺序都跑。
固定提示写法再跑一次做排除
把三类任务的提示统一改成同一模板:一句角色说明、一句任务说明、一句输出格式要求、一句禁止项,只替换任务正文。改完后用同样的三组顺序、同样的固定输入、同样的接入参数再跑一遍,结果存到 runs_2/ 之类的独立目录,避免和第一轮混在一起。
角色:你在执行单项任务,忽略会话中此前任务的要求。
任务:<此处只替换任务正文>
输出格式:<结构化任务写 JSON 字段;写作任务写体裁与字数区间;追问任务写明本轮只回答当前问题>
禁止:不要解释过程,不要复用上一任务的格式。
对比时只看同一任务在两次运行中的差异:长度是否接近、结构是否重新守约、答偏是否消失。此时结论成立需要几个条件同时满足——接入地址和模型版本未变、采样参数未变、固定输入逐字未改、每组顺序的会话长度一致、两次运行只改了提示写法。若不满足,差异无法归因到提示写法上。
结果的读法:改写法后下滑消失或明显后移,说明提示写法是主要变量,先把模板固化成默认写法再谈多能力切换;改写法后下滑位置和表现基本不变,说明更可能是会话内任务切换或上下文累积的影响,此时保守做法是把差异大的任务拆到不同会话执行,并保留本轮记录作为后续复测的基线。