Prompt 链与多步工作流编排
复杂任务一次 LLM 调用搞不定,要拆成多步链——但链怎么拆、中间结果怎么传、错误怎么恢复,是工程问题。本文讲清 prompt chaining 的几种模式(顺序/并行/路由/循环)、何时该链何时该用 Agent、中间状态管理,以及和单次调用的成本/质量权衡。
本文要点
- 主题聚焦:AI、AI Agent
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
"帮我研究 X,写个大纲,再写一篇 3000 字文章,最后润色。"——把这种任务塞进一次 LLM 调用,你大概率得到一份样样都平庸的东西:研究浅、大纲乱、正文散、润色等于没润色。复杂任务不是"prompt 写得够长"就能解决,而是要拆成多步链,让每一步只做一件事、只戴一顶帽子。
一、为什么单次调用不够用
单次调用的瓶颈有三个,且都不是"加 token"能解决的:
- 任务复杂度溢出。 一个 prompt 同时要求"检索+推理+写作+自检",每多一个目标,模型在每个目标上的注意力都被稀释。实测里,"一次写完"的输出几乎总是每项都 70 分。
- 上下文污染。 把研究材料、大纲、正文、修改意见全堆进一个上下文,模型容易抄前面的废话、忘后面的约束。长上下文 ≠ 长记忆,中间的信息会被"淹没"。
- 多角色冲突。 研究员要发散、编辑要收敛、校对要挑刺——这些指令互相打架,塞一个 prompt 里模型只能和稀泥。
拆链的本质是:把一个高方差任务,切成几个低方差子任务,每个子任务单独可测、单独可调、单独可换模型。
二、链的四种基本模式
所有 prompt chain 都是这四种模式的组合。
顺序 Sequential A → B → C → D
并行 Fan-out A → [B,C,D 并行] → merge
路由 Router A → (条件) → B | C | D
循环 Loop A → B → (满意?) ─ 否 ─→ B ...
└ 是 ─→ 输出
顺序链:最常见。上一步输出喂下一步,如"翻译→校对→排版"。适合步骤固定、前后依赖明确的流程。
并行链(fan-out):同一步派生多个独立子任务并行跑,再汇总。如"对 10 个章节各自生成摘要,再合并成总纲"。延迟低、吞吐高,但汇总会丢失细节,适合可合并的任务。
路由链:根据输入走不同分支。如"先分类:技术问题走专家 prompt,闲聊走轻量 prompt,违规走拒绝"。能省 token(简单任务不上大模型)、提质量(每分支专用 prompt)。
循环链:反复迭代直到满足退出条件。如"生成→自评→不够好→带着批评重新生成"。质量上限最高,但延迟和成本不可控,必须设硬上限(最多 N 轮)。
一个最小骨架:
type Step<I, O> = (input: I) => Promise<O>;
// 顺序
const chain = async (topic: string) => {
const research = await researchStep(topic);
const outline = await outlineStep(research);
const draft = await draftStep(outline);
return polishStep(draft);
};
// 并行 fan-out + merge
const summarizeMany = async (chunks: string[]) => {
const parts = await Promise.all(chunks.map(summarizeStep));
return mergeStep(parts);
};
// 路由
const route = async (q: string) => {
const kind = await classifyStep(q); // 'tech' | 'chat' | 'block'
return kind === 'tech' ? expertStep(q)
: kind === 'chat' ? lightStep(q)
: refuseStep();
};
// 循环(带硬上限)
const refine = async (topic: string) => {
let draft = await draftStep(topic);
for (let i = 0; i < 3; i++) {
const critique = await critiqueStep(draft);
if (critique.score >= 8) break;
draft = await reviseStep(draft, critique);
}
return draft;
};
三、链 vs Agent:别把链包装成 Agent
链和 Agent 经常被混为一谈,但它们的工程取舍完全相反:
- 链是确定流程:每一步是什么、顺序如何、传什么,你在写代码时就定死了。模型只负责"把这一步做好",不负责"决定下一步干嘛"。可预测、可重放、好调试。
- Agent 是自主决策:模型自己看情况决定调哪个工具、要不要再查一轮、什么时候收手。灵活,但不可预测、难调试、贵。
经验法则:流程越确定,越该用链;越开放探索,越该用 Agent。 "研究→大纲→起草→润色"是确定的,用链;"帮我查清楚这个 bug 的根因,需要查代码就查、需要跑用例就跑"是开放的,用 Agent。
很多人一上来就上 Agent,结果模型每轮都在"决定下一步干嘛"上花 token、还会绕远路。先用链把确定性流程钉死,只在真正需要决策的岔路口上引入 Agent,是性价比最高的设计。一个折中:链做骨架,某个节点内部是 Agent(如"研究"这一步让 Agent 自主检索),其余节点保持简单确定性。
四、中间状态:结构化传递,别串接自然语言
链最容易翻车的地方,是上一步输出怎么交给下一步。新手做法是"把 A 的自然语言输出直接拼进 B 的 prompt"——这等于在步骤之间加了一个高噪声通道。
// ❌ 反例:自然语言串接,噪声大、不可解析
const outline = await llm(`基于这些研究写大纲:${researchText}`);
const draft = await llm(`基于这个大纲写正文:${outline}`);
// ✅ 正例:结构化 schema 传递,B 只取需要的字段
type Research = { keyFacts: string[]; sources: {title:string;url:string}[]; gaps: string[] };
type Outline = { sections: { heading: string; points: string[] }[] };
const research: Research = await researchStep(topic); // 强制 JSON schema
const outline: Outline = await outlineStep(research); // 只读 research.keyFacts
const draft = await draftStep(outline);
结构化传递有三个直接好处:B 步 prompt 可以只裁剪需要的字段(别全量传)、中间结果可落库可重放(某步炸了不用从头跑)、每步可独立评测。
上下文裁剪是被低估的优化。不要把研究阶段产出的 5KB 原文一股脑塞进写作阶段——写作只需要 keyFacts,校对只需要 draft。每步只给"这一步真正需要的最小上下文",质量反而更高(模型不分心),token 也更省。
五、错误恢复:每一步都可能炸
链一旦有 N 步,整体失败概率是 1 - (1-p)^N——单步 2% 失败率,十步链就有 18% 的概率某步出问题。不做错误恢复的链上不了生产。
每一步要明确失败策略,四选一(或组合):
- 重试:网络抖动、JSON 解析失败这类瞬时错误,指数退避重试 2–3 次。注意重试要换温度或加"上次错在哪"的反馈,否则同样的输入同样的错。
- 降级:大模型挂了走小模型、JSON 解析失败走正则兜底、主链路挂了走缓存结果。降级要可接受质量下滑。
- 跳过:非关键步骤(如"配图建议")失败就跳过,主流程继续。
- 人工兜底:高价值/不可逆步骤(如最终发布)失败,进人工队列。
const withRetry = async <T>(fn: () => Promise<T>, times = 3): Promise<T> => {
let lastErr: unknown;
for (let i = 0; i < times; i++) {
try { return await fn(); }
catch (e) { lastErr = e; await sleep(2 ** i * 500); }
}
throw lastErr;
};
// 幂等:中间结果落库,按 (taskId, stepName, inputHash) 去重
const runStep = async (taskId: string, step: string, input: unknown) => {
const key = `${taskId}:${step}:${hash(input)}`;
const cached = await db.get(key);
if (cached) return cached; // 重放时跳过已完成步
const out = await withRetry(() => steps[step](input));
await db.set(key, out);
return out;
};
幂等是链的生命线。把每步的输入算 hash、输出落库,任何一步炸了重跑都能从断点续上,而不是从头烧 token。这比任何重试逻辑都值钱。
六、成本与延迟:链的隐形账单
链不是免费的。N 步链的延迟是各步之和(顺序时)、token 是各步之和,很容易比单次调用贵 3–5 倍。可控优化手段:
- 能并行就并行。 顺序链里但凡有"互相独立"的子任务,果断 fan-out,用
Promise.all。延迟从"串行之和"变"最慢一个"。 - 缓存中间结果。 同一输入的"研究""分类"结果可缓存复用,尤其路由链的分类步,命中率极高。
- 简单步用小模型。 路由分类、JSON 抽取、格式校验这类活,Haiku 级模型够用;只在推理/写作这种重活上用 Opus/Sonnet。混搭下来成本能砍一半以上。
- prompt caching。 链里每步的 system prompt 通常稳定,开 prompt caching 把前缀缓存住,重复调用省一大笔。
一句话:链的账要算总账——质量提升带来的收益(返工少、人工干预少)能不能盖过 token 和延迟的增加,盖得过就上链,盖不过就老老实实单次调用。
七、实战:一条"研究→大纲→起草→润色"内容生成链
把前面的东西捏到一起。这是我自己在用的内容生成链骨架:
type Ctx = { topic: string; research?: Research; outline?: Outline;
draft?: string; article?: string };
const generateArticle = async (topic: string): Promise<string> => {
let ctx: Ctx = { topic };
// 1. 研究:Agent 子步,自主检索(开放探索 → 用 Agent)
ctx.research = await runStep(topic, 'research', topic);
// 2. 大纲:确定性步,裁剪上下文,只传 keyFacts
ctx.outline = await runStep(topic, 'outline', ctx.research.keyFacts);
// 3. 起草:循环链,自评+修订,最多 2 轮
ctx.draft = await runStep(topic, 'draft',
{ outline: ctx.outline, facts: ctx.research.keyFacts, maxRounds: 2 });
// 4. 润色:小模型也能做,换便宜模型 + prompt caching
ctx.article = await runStep(topic, 'polish', ctx.draft);
return ctx.article;
};
设计取舍一目了然:开放的研究步用 Agent、确定的写作步用链;每步结构化进出、落库可重放;循环带硬上限;润色降级到小模型。这条链跑出来的文章,质量稳定碾压"一次写完"的版本,因为每一步都只在一个明确目标上用力。
小结
- 拆链的判据:任务有多个互相干扰的目标、上下文会互相污染、需要不同角色——满足任一就该考虑链。
- 四种模式:顺序、并行、路由、循环,记住这四块积木,99% 的场景拼得出来。
- 链 vs Agent:确定流程用链,开放探索用 Agent;先用链钉死骨架,只在岔路口上 Agent。
- 中间状态:结构化 schema 传递,每步只给最小上下文,中间结果落库可重放。
- 错误恢复靠幂等:
(taskId, step, inputHash)去重 + 重试/降级/跳过/人工,比任何重试逻辑都值钱。 - 算总账:质量收益要盖过 token 和延迟成本;并行、缓存、小模型混搭是三件套。
金句:单次调用是许愿,链是分工,Agent 是授权。 把复杂任务从"一次赌对"变成"步步可控",你才真的拥有它,而不是被它的输出牵着走。
Related