Prompt 的 Chain 设计——把复杂任务拆成多个 Prompt 顺序处理
Prompt 的 Chain 设计——把复杂任务拆成多个 Prompt 顺序处理
有一个合同分析的需求,产品经理最初的要求是这样的:用户上传一份合同,AI 自动输出:合同类型和适用范围、关键条款清单、潜在法律风险、谈判建议和修改意见、执行摘要(面向 C 级高管)。
第一版实现很粗暴:一个超长的 Prompt,把所有要求全塞进去,丢给 GPT-4o,然后期待它输出一份完整的分析报告。
结果很糟糕。模型确实输出了五个部分,但每个部分的质量参差不齐。执行摘要写得还行,但谈判建议几乎是废话,风险分析缺乏具体条款引用。最关键的问题是:任务之间有依赖关系——谈判建议必须基于风险分析,但在同一个 Prompt 里,模型并不能真正"先做完风险分析,再做谈判建议"。
第二版,我把这个任务拆成了 5 个独立的 Prompt,每个 Prompt 只做一件事,前一步的输出作为后一步的输入。质量提升显著,而且每一步都可以单独检查和调试。
这就是 Prompt Chain 的核心价值:把依赖关系显式化,每步只做一件事,中间结果可验证。
单个长 Prompt vs Prompt Chain 的本质区别
单个长 Prompt 的问题
LLM 在处理长 Prompt 时有几个固有限制:
注意力稀释:Transformer 的注意力机制在面对超长输入时,对早期内容的注意力会自然稀释。一个 4000 Token 的 Prompt,后半段的指令可能没有前半段被"认真对待"。
任务干扰:多个任务写在一个 Prompt 里时,它们会相互干扰。让模型同时保持"合同律师的严谨视角"和"高管的宏观视角",实际上是在要求它分裂思维。
无中间验证点:一个长 Prompt 生成的结果,如果最终输出有问题,你很难定位是哪个环节出了错。
Prompt Chain 的优势
每个 Prompt 只做一件事,意味着:
- 每个 Prompt 都可以针对单一任务深度优化
- 前一步的输出可以被验证、过滤或增强后再传给下一步
- 某步失败时,可以精确定位和重试
- 可以在链的任意位置插入人工审核
代价是:总体 Token 消耗增加(因为上下文要重复传递),以及更多的 API 调用次数(延迟增加)。
合同分析 Chain 的完整设计
任务分解
把合同分析任务拆解成以下步骤:
每步的职责:
- Step 1:提取合同基本信息(类型、当事方、有效期、金额等),输出结构化 JSON
- Step 2:分析关键条款,每条款给出条款原文 + 含义解读
- Step 3:基于 Step 2 的条款分析,识别法律风险点
- Step 4:基于 Step 3 的风险,生成针对性的谈判建议
- Step 5:将所有步骤的结果综合为高管可读的执行摘要
核心框架:Prompt Chain 执行器
@Component
@Slf4j
public class PromptChainExecutor {
private final ChatClient chatClient;
public PromptChainExecutor(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 执行 Prompt Chain
* @param chain 链定义(有序的步骤列表)
* @param initialInput 初始输入
*/
public ChainExecutionResult execute(PromptChain chain, String initialInput) {
ChainExecutionResult result = new ChainExecutionResult();
result.setChainId(chain.getId());
result.setStartTime(LocalDateTime.now());
Map<String, Object> context = new HashMap<>();
context.put("initial_input", initialInput);
String currentInput = initialInput;
for (int i = 0; i < chain.getSteps().size(); i++) {
ChainStep step = chain.getSteps().get(i);
log.info("执行 Chain 步骤 {}/{}: {}", i + 1, chain.getSteps().size(), step.getName());
StepExecutionRecord record = executeStep(step, currentInput, context);
result.addStepRecord(record);
if (!record.isSuccess()) {
// 步骤执行失败的处理策略
if (step.isRequired()) {
log.error("必要步骤 {} 执行失败,终止 Chain", step.getName());
result.setSuccess(false);
result.setFailedStep(step.getName());
break;
} else {
log.warn("可选步骤 {} 执行失败,跳过继续", step.getName());
// 可选步骤失败时,传递上一步的输出
continue;
}
}
// 把当前步骤的输出注入 context,供后续步骤使用
context.put(step.getOutputKey(), record.getOutput());
// 当前步骤的输出作为下一步骤的主要输入
currentInput = record.getOutput();
// 中间结果校验
if (step.getValidator() != null) {
ValidationResult validation = step.getValidator().validate(record.getOutput());
if (!validation.isValid()) {
log.warn("步骤 {} 校验未通过:{}", step.getName(), validation.getMessage());
record.setValidationWarning(validation.getMessage());
if (validation.isCritical()) {
// 如果是关键校验,尝试重试
record = retryStep(step, currentInput, context, 2);
if (!record.isSuccess()) {
result.setSuccess(false);
result.setFailedStep(step.getName());
break;
}
context.put(step.getOutputKey(), record.getOutput());
currentInput = record.getOutput();
}
}
}
}
if (result.getFailedStep() == null) {
result.setSuccess(true);
result.setFinalOutput(currentInput);
}
result.setEndTime(LocalDateTime.now());
return result;
}
private StepExecutionRecord executeStep(ChainStep step,
String input,
Map<String, Object> context) {
StepExecutionRecord record = new StepExecutionRecord();
record.setStepName(step.getName());
record.setInput(input);
record.setStartTime(LocalDateTime.now());
try {
// 构建这一步的 Prompt
String prompt = step.getPromptBuilder().buildPrompt(input, context);
// 调用 LLM
String output = chatClient.prompt()
.system(step.getSystemPrompt())
.user(prompt)
.call()
.content();
// 后处理(如格式转换、截断等)
if (step.getPostProcessor() != null) {
output = step.getPostProcessor().process(output);
}
record.setOutput(output);
record.setSuccess(true);
} catch (Exception e) {
log.error("步骤 {} 执行异常", step.getName(), e);
record.setSuccess(false);
record.setError(e.getMessage());
}
record.setEndTime(LocalDateTime.now());
return record;
}
private StepExecutionRecord retryStep(ChainStep step, String input,
Map<String, Object> context, int maxRetries) {
for (int retry = 0; retry < maxRetries; retry++) {
log.info("重试步骤 {},第 {}/{} 次", step.getName(), retry + 1, maxRetries);
StepExecutionRecord record = executeStep(step, input, context);
if (record.isSuccess()) {
return record;
}
}
// 所有重试都失败
StepExecutionRecord failedRecord = new StepExecutionRecord();
failedRecord.setStepName(step.getName());
failedRecord.setSuccess(false);
failedRecord.setError("超过最大重试次数");
return failedRecord;
}
}合同分析 Chain 的具体定义
@Component
public class ContractAnalysisChainFactory {
public PromptChain createContractAnalysisChain() {
return PromptChain.builder()
.id("contract-analysis-v1")
.steps(Arrays.asList(
buildStep1ContractInfoExtraction(),
buildStep2KeyClauseAnalysis(),
buildStep3RiskIdentification(),
buildStep4NegotiationAdvice(),
buildStep5ExecutiveSummary()
))
.build();
}
private ChainStep buildStep1ContractInfoExtraction() {
return ChainStep.builder()
.name("合同基本信息提取")
.outputKey("contract_info")
.required(true)
.systemPrompt("""
你是一名合同信息抽取专家。
你的任务是从合同文本中提取结构化的基本信息。
严格按照 JSON 格式输出,不要有任何额外文字。
""")
.promptBuilder((input, context) -> """
请从以下合同中提取基本信息,以 JSON 格式输出:
{
"contract_type": "合同类型",
"parties": [{"role": "甲方/乙方", "name": "名称"}],
"effective_date": "生效日期",
"expiry_date": "到期日期",
"contract_amount": "合同金额",
"governing_law": "适用法律",
"jurisdiction": "管辖法院或仲裁机构"
}
合同文本:
%s
""".formatted(input))
.validator(output -> {
// 验证输出是有效的 JSON
try {
new ObjectMapper().readTree(output);
return ValidationResult.valid();
} catch (Exception e) {
return ValidationResult.critical("输出不是有效的 JSON: " + e.getMessage());
}
})
.build();
}
private ChainStep buildStep2KeyClauseAnalysis() {
return ChainStep.builder()
.name("关键条款分析")
.outputKey("clause_analysis")
.required(true)
.systemPrompt("""
你是一名专注于合同条款分析的法律顾问。
你的任务是识别并解读合同中的关键条款。
输出需要覆盖:付款条款、违约条款、知识产权条款、保密条款、
责任限制条款、合同解除条款等重要类型(如有)。
""")
.promptBuilder((input, context) -> {
// 把第一步提取的合同信息作为背景知识
String contractInfo = (String) context.getOrDefault(
"contract_info", "无合同基本信息");
String originalContract = (String) context.get("initial_input");
return """
合同基本信息:
%s
请分析以下合同中的关键条款,对每个关键条款给出:
1. 条款标题
2. 条款原文(引用)
3. 含义解读(用通俗语言解释)
4. 对双方的影响
合同全文:
%s
""".formatted(contractInfo, originalContract);
})
.build();
}
private ChainStep buildStep3RiskIdentification() {
return ChainStep.builder()
.name("风险识别")
.outputKey("risk_analysis")
.required(true)
.systemPrompt("""
你是一名合同风险识别专家,代表合同甲方的利益。
你的任务是基于条款分析,识别对甲方可能构成不利的风险点。
风险等级分为:高风险(可能导致重大损失)、中风险(需要关注)、低风险(可接受)。
""")
.promptBuilder((input, context) -> {
// 这里 input 是第二步的输出(条款分析结果)
return """
基于以下关键条款分析,识别对甲方的潜在风险:
%s
请按风险等级(高/中/低)列出每个风险点:
- 风险描述(具体指向哪个条款)
- 风险性质(法律风险/财务风险/运营风险)
- 潜在影响(最坏情况下的后果)
- 风险等级
""".formatted(input);
})
.validator(output -> {
// 简单验证:确保包含风险相关关键词
if (!output.contains("风险") && !output.contains("risk")) {
return ValidationResult.warning("输出中未找到风险相关内容");
}
return ValidationResult.valid();
})
.build();
}
private ChainStep buildStep4NegotiationAdvice() {
return ChainStep.builder()
.name("谈判建议生成")
.outputKey("negotiation_advice")
.required(false) // 这一步不是必须的,如果失败可以跳过
.systemPrompt("""
你是一名有丰富实战经验的合同谈判顾问。
基于已识别的风险,提供具体、可操作的谈判建议。
建议必须针对具体条款,并提供修改方向或替代表述。
""")
.promptBuilder((input, context) -> {
// input 是风险分析结果
String clauseAnalysis = (String) context.getOrDefault(
"clause_analysis", "");
return """
基于以下风险分析,提供具体的谈判建议:
风险分析:
%s
关键条款分析(供参考):
%s
请为每个高风险点提供:
1. 谈判目标(希望达成的修改方向)
2. 建议的替代表述(具体的修改文本)
3. 谈判策略(如何在谈判桌上提出这个修改请求)
4. 底线(最低可接受的修改幅度)
""".formatted(input, clauseAnalysis);
})
.build();
}
private ChainStep buildStep5ExecutiveSummary() {
return ChainStep.builder()
.name("执行摘要生成")
.outputKey("executive_summary")
.required(true)
.systemPrompt("""
你是一名商业顾问,需要为公司高管(C级)准备合同分析摘要。
摘要要简洁、重点突出,聚焦于商业决策所需的关键信息。
高管没有时间看详细法律分析,只需要:是否签署、主要风险、建议行动。
""")
.promptBuilder((input, context) -> {
// 整合所有前序步骤的结果
String contractInfo = (String) context.getOrDefault("contract_info", "");
String riskAnalysis = (String) context.getOrDefault("risk_analysis", "");
String negotiationAdvice = (String) context.getOrDefault(
"negotiation_advice", "暂无谈判建议");
return """
请基于以下完整分析,生成一份供高管阅读的执行摘要(不超过 500 字):
合同基本信息:
%s
主要风险:
%s
谈判建议:
%s
摘要应包含:
1. 本合同概述(一句话)
2. 主要风险(最多 3 条,按重要性排序)
3. 建议行动(可签署/需修改后签署/建议不签,以及理由)
4. 如需谈判,最优先要求的 1-2 项修改
""".formatted(contractInfo, riskAnalysis, negotiationAdvice);
})
.build();
}
}Chain 执行和结果整合
@Service
@Slf4j
public class ContractAnalysisService {
private final PromptChainExecutor chainExecutor;
private final ContractAnalysisChainFactory chainFactory;
public ContractAnalysisReport analyze(String contractText) {
PromptChain chain = chainFactory.createContractAnalysisChain();
log.info("开始合同分析 Chain,合同长度:{} 字符", contractText.length());
long startTime = System.currentTimeMillis();
ChainExecutionResult executionResult = chainExecutor.execute(chain, contractText);
long duration = System.currentTimeMillis() - startTime;
log.info("合同分析完成,总耗时:{} ms,成功:{}", duration, executionResult.isSuccess());
// 构建最终报告
return buildReport(executionResult, duration);
}
private ContractAnalysisReport buildReport(ChainExecutionResult result, long duration) {
ContractAnalysisReport report = new ContractAnalysisReport();
// 从每步结果中提取数据
for (StepExecutionRecord record : result.getStepRecords()) {
switch (record.getStepName()) {
case "合同基本信息提取" -> report.setContractInfo(record.getOutput());
case "关键条款分析" -> report.setClauseAnalysis(record.getOutput());
case "风险识别" -> report.setRiskAnalysis(record.getOutput());
case "谈判建议生成" -> report.setNegotiationAdvice(record.getOutput());
case "执行摘要生成" -> report.setExecutiveSummary(record.getOutput());
}
}
report.setAnalysisDurationMs(duration);
report.setStepCount(result.getStepRecords().size());
report.setGeneratedAt(LocalDateTime.now());
return report;
}
}中间结果验证的工程价值
Prompt Chain 里,每个步骤之间的验证是关键。我把验证分为三类:
格式验证:检查输出是否符合预期格式(JSON、特定字段存在等)。如果格式错了,后续步骤直接用这个输出会崩。
语义验证:检查输出内容是否有意义。比如风险分析应该至少包含一个风险点,如果输出只有"无风险",大概率是模型在偷懒,需要重试。
业务规则验证:特定于业务的校验。比如合同金额必须是正数,日期格式必须符合规范等。
@Component
public class ChainValidators {
/**
* JSON 格式校验器
*/
public static StepValidator jsonValidator() {
return output -> {
try {
new ObjectMapper().readTree(output);
return ValidationResult.valid();
} catch (Exception e) {
return ValidationResult.critical("JSON 格式错误: " + e.getMessage());
}
};
}
/**
* 最小内容长度校验
*/
public static StepValidator minLengthValidator(int minLength) {
return output -> {
if (output == null || output.trim().length() < minLength) {
return ValidationResult.warning("输出内容过短(" +
(output != null ? output.length() : 0) + " < " + minLength + ")");
}
return ValidationResult.valid();
};
}
/**
* 必含关键词校验
*/
public static StepValidator containsKeywordsValidator(List<String> keywords) {
return output -> {
List<String> missing = keywords.stream()
.filter(kw -> !output.contains(kw))
.collect(Collectors.toList());
if (!missing.isEmpty()) {
return ValidationResult.warning("缺少关键内容:" + missing);
}
return ValidationResult.valid();
};
}
/**
* 不含拒绝性语言校验(防止模型拒绝执行任务)
*/
public static StepValidator noRefusalValidator() {
List<String> refusalPatterns = Arrays.asList(
"我无法", "我不能", "这超出了", "作为AI", "I cannot", "I'm unable"
);
return output -> {
boolean hasRefusal = refusalPatterns.stream()
.anyMatch(output::contains);
if (hasRefusal) {
return ValidationResult.critical("模型拒绝执行任务,需要修改 Prompt");
}
return ValidationResult.valid();
};
}
}并行 Chain vs 串行 Chain
并不是所有任务都需要串行。如果某些步骤之间没有依赖关系,可以并行执行:
@Service
public class ParallelChainExecutor {
private final PromptChainExecutor sequentialExecutor;
/**
* 混合执行:串行步骤和并行步骤的组合
*
* 示例:合同分析改进版
* - Step 1(串行):合同信息提取
* - Step 2 & 3(并行):条款分析 + 合规性检查(互不依赖)
* - Step 4(串行,依赖 2 和 3):风险识别
* - Step 5(串行):执行摘要
*/
public ChainExecutionResult executeWithParallelGroups(List<ChainGroup> groups,
String initialInput) {
ChainExecutionResult totalResult = new ChainExecutionResult();
Map<String, Object> sharedContext = new HashMap<>();
sharedContext.put("initial_input", initialInput);
String currentInput = initialInput;
for (ChainGroup group : groups) {
if (group.isParallel()) {
// 并行执行组内步骤
Map<String, StepExecutionRecord> parallelResults =
executeParallelGroup(group, currentInput, sharedContext);
// 合并结果
for (Map.Entry<String, StepExecutionRecord> entry : parallelResults.entrySet()) {
sharedContext.put(entry.getKey(), entry.getValue().getOutput());
totalResult.addStepRecord(entry.getValue());
}
// 并行组之后,把所有并行输出拼接作为下一步输入
currentInput = parallelResults.values().stream()
.map(StepExecutionRecord::getOutput)
.collect(Collectors.joining("\n\n---\n\n"));
} else {
// 串行执行
for (ChainStep step : group.getSteps()) {
StepExecutionRecord record = executeStep(step, currentInput, sharedContext);
totalResult.addStepRecord(record);
if (record.isSuccess()) {
sharedContext.put(step.getOutputKey(), record.getOutput());
currentInput = record.getOutput();
}
}
}
}
totalResult.setFinalOutput(currentInput);
return totalResult;
}
private Map<String, StepExecutionRecord> executeParallelGroup(ChainGroup group,
String input,
Map<String, Object> context) {
Map<String, CompletableFuture<StepExecutionRecord>> futures = new HashMap<>();
for (ChainStep step : group.getSteps()) {
CompletableFuture<StepExecutionRecord> future =
CompletableFuture.supplyAsync(() -> executeStep(step, input, context));
futures.put(step.getOutputKey(), future);
}
Map<String, StepExecutionRecord> results = new HashMap<>();
for (Map.Entry<String, CompletableFuture<StepExecutionRecord>> entry : futures.entrySet()) {
try {
results.put(entry.getKey(), entry.getValue().get(60, TimeUnit.SECONDS));
} catch (Exception e) {
log.error("并行步骤执行失败:{}", entry.getKey(), e);
StepExecutionRecord failed = new StepExecutionRecord();
failed.setSuccess(false);
failed.setError(e.getMessage());
results.put(entry.getKey(), failed);
}
}
return results;
}
private StepExecutionRecord executeStep(ChainStep step, String input,
Map<String, Object> context) {
// 委托给串行执行器的单步执行逻辑
// 这里省略重复代码
return new StepExecutionRecord();
}
}成本控制:什么时候用 Chain,什么时候用单 Prompt
最后说一个经常被忽视的问题:Chain 不是越复杂越好。
Chain 的成本:
- 每个步骤一次 API 调用,N 步就是 N 次
- 上下文在步骤间传递,Token 累积增长
- 延迟增加(即使并行,也有串行瓶颈)
什么时候用 Chain:
- 任务有明确的依赖关系(后步需要前步的结果)
- 单个 Prompt 的质量不稳定或不够好
- 需要在中间步骤插入人工审核
- 调试和可观测性要求高
什么时候用单 Prompt:
- 任务本质上是单步的(分类、摘要、翻译)
- 延迟敏感的实时场景
- 成本敏感的高频场景
合同分析这个案例,5 步 Chain 比单 Prompt 的成本高 4-5 倍,但质量提升明显。对于分析一份合同这类低频、高价值的场景,这个成本是值得的。对于每天处理数万条的日常分类任务,就不适合用 Chain 了。
