AI Prompt 工程化设计最佳实践(Harness Engineering)
1. 核心理念把 Prompt 当作代码大多数开发者与 LLM 交互时是把 prompt 当作自然语言对话来写的❌ 帮我生成一张图片内容是一个苹果风格可爱工程化的做法是把 prompt 当作代码来管理——有输入、有处理逻辑、有输出格式、有错误处理✅ 输入 → 分类 → 路由到对应模板 → 结构化参数注入 → 组装 → 最终 prompt核心心态转变你不是在和 AI 聊天你是在编程驱动 AI。这引出了第一条也是最根本的原则。2. 原则一Plan-and-Prompt 分离问题当 LLM 直接生成最终 prompt 时你无法控制哪些内容应该出现、哪些不应该约束是否被遵守输出格式是否一致模式┌──────────────────────────────────────────────────┐ │ 你的代码 │ │ │ │ 输入 ──→ Plan阶段(LLM) ──→ 结构化结果 │ │ │ │ │ ▼ │ │ Build阶段(纯代码) │ │ │ │ │ ▼ │ │ 最终 Prompt │ │ │ │ │ ▼ │ │ 执行阶段(LLM / 图像模型 / ...) │ │ │ └──────────────────────────────────────────────────┘Plan 阶段LLM将高层次需求转为结构化 JSON。LLM 只做语义转换。Build 阶段纯代码将结构化规格确定性地组装成最终 prompt。不经过 LLM。Execute 阶段目标模型将最终 prompt 发送给图像/文本/代码模型。为什么这样做维度LLM 直接生成 promptPlan-and-Prompt 分离可控性低——LLM 可能忽略约束高——代码控制最终 prompt 的每个词可调试性低——不知道是理解错了还是表达错了高——可以分别检查 Plan 和 Final Prompt约束遵守不可靠——LLM 经常忘记负向约束代码保证约束一定出现在最终输出中一致性低——每次输出格式可能不同代码保证输出格式始终一致可测试性难——只能端到端测试易——Build 阶段可单独单元测试代码示例# ❌ 反模式让 LLM 直接生成最终 prompt response llm.chat(fGenerate an image prompt for: {user_input}) final_prompt response.text # 不可控 # ✅ Plan-and-Prompt 分离 plan llm.chat_json( systemExtract visual semantics from the input as JSON., useruser_input ) # plan {mainSubject: a red apple, style: flat illustration, ...} final_prompt build_prompt(plan) # 纯代码确定性 # A simple flat illustration of a red apple. Clean lines, centered...适用场景任何需要精确控制最终 prompt 的场景文生图、文生视频、代码生成、文档生成需要遵守敏感词、合规、品牌安全规则的场景需要支持 A/B 测试 prompt 变体的场景Agent / Tool-calling 的中间步骤规划3. 原则二多阶段流水线问题单一 LLM 调用试图完成理解输入 规划内容 格式化输出三件事每一步的失败都会污染下一步。模式将复杂任务拆分成独立、可替换、可单独测试的阶段Stage 1 Stage 2 Stage 3 Stage 4 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Parse │───▶│ Classify │───▶│ Plan │───▶│ Build │ │ (纯代码) │ │ (纯代码) │ │ (LLM) │ │ (纯代码) │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ 输入 类型 规格 最终格式每个阶段的职责阶段执行者职责输入 → 输出Parse纯代码提取结构化信息原始字符串 → 结构化对象Classify纯代码判断类型/意图结构化对象 → 分类标签PlanLLM语义转换、内容规划结构化对象 分类 → JSON 规格Build纯代码Prompt 组装JSON 规格 → 最终 prompt判断是否需要拆分的经验法则问题答案这个判断能用 if/else 做吗→ 不要用 LLM用代码这个转换需要理解语义吗→ 用 LLM这个格式化有固定格式要求吗→ 用代码不要依赖 LLM 记忆格式这个约束绝对不能违反吗→ 用代码硬编码不交给 LLM代码示例# ❌ 反模式一个 prompt 做所有事 response llm.chat(f Analyze this input: {user_input} Classify its type Plan the visual content Output a final image prompt ) # ✅ 多阶段流水线 parsed parse_input(user_input) # Stage 1: 纯代码解析 category classify(parsed) # Stage 2: 纯代码分类 plan llm_plan(parsed, category) # Stage 3: LLM 语义规划 prompt build_prompt(plan, category) # Stage 4: 纯代码组装4. 原则三Schema 即约束核心洞察LLM 输出的 JSON Schema 不只是数据格式——它是对 LLM 行为的最强约束。Schema 里有什么字段LLM 就会去思考什么Schema 里没有的字段LLM 就不会考虑。你可以通过选择性地暴露或隐藏字段来精确控制 LLM 的注意力范围。实践按场景使用不同 Schema# ❌ 反模式万能 Schema——包含所有场景的字段 universal_schema { overlayText: ..., # 字母卡才需要 allowVisibleText: True, # 大部分场景不需要 sceneDescription: ..., characterCount: 0, # 只有人物场景需要 # ... 10 个字段 } # → LLM 会为所有字段分配注意力包括不相关的 # ✅ 按场景使用精简 Schema if category letter_card: schema { letter: ..., illustration: ..., allowText: True } elif category scene_card: schema { action: ..., setting: ..., mood: ... } # 没有 allowText 字段 → LLM 根本不会考虑要不要写字为什么字段的存在与否比值为 false更有效如果 schema 中有allowText: falseLLM 仍然看到了allowText这个字段名已经被提示了文字这个概念如果 schema 中根本没有allowText字段LLM 的注意力完全不会被引导到文字方向同样的逻辑适用于任何你不希望 LLM 考虑的维度政治、暴力、品牌、价格……Schema 设计检查清单每个字段都是当前任务必需的吗不需要的就删掉字段名是否在引导正确的思维方向避免text,label,caption等词当任务不需要文字时是否有冗余字段可以用一个字段替代字段顺序是否反映了优先级LLM 通常更关注前面的字段5. 原则四Prompt 模块化组装问题把整个 prompt 写成一个长字符串或一个模板难以增删某个约束根据条件切换某一段单独调试某一部分做 A/B 测试模式Section 化将 prompt 分解为语义独立的 Section每个 Section 是 List 中的一个元素sections [ # Section 1: 全局格式硬约束前置——最重要 The output must be a valid JSON array., # Section 2: 风格指令正向引导 Use professional, concise language., # Section 3: 条件性指令仅在需要时加入 *([Include citations for each claim.] if require_citations else []), # Section 4: 负向约束放在后面——安全网角色 Do not include personal opinions. Do not speculate., ] # 组装过滤空字符串用换行符连接 final_prompt \n.join(s for s in sections if s)Section 的组织原则位置内容类型原因最前前25%全局格式要求、关键硬约束LLM 对 prompt 前半部分关注度更高中间任务描述、正向引导告诉 LLM 要做什么条件根据上下文动态添加的指令按业务逻辑判断是否加入靠后负向约束、禁止列表防止稀释主体指令充当安全网最后输入数据 / 用户内容避免被误解释为指令的一部分为什么不用模板引擎模板引擎Jinja / Handlebars适合文本填空但不适合 prompt 工程条件逻辑写在模板里会变得难以阅读{% if %}嵌套模板不容易做 Section 级别的 A/B 测试模板的空白符控制经常出问题推荐做法在代码中构建list[str]最后join。每个 Section 是一行代码清晰、可调试、可单测。代码示例条件性 Sectiondef build_system_prompt(user_role: str, task_type: str, include_examples: bool) - str: sections [You are a helpful assistant. Be concise and accurate.] # 按角色定制 if user_role expert: sections.append(Use technical terminology appropriate for domain experts.) elif user_role beginner: sections.append(Explain concepts in simple terms. Avoid jargon.) # 按任务类型定制 if task_type creative: sections.append(Be imaginative and explore multiple angles. Up to 500 words.) elif task_type analytical: sections.append(Be rigorous and evidence-based. Cite sources. Up to 300 words.) # 可选示例 if include_examples: sections.append(Include 1-2 concrete examples in your response.) return \n\n.join(sections)6. 原则五代码覆写 LLM 输出核心原则LLM 提供建议代码做裁决。永远不要信任 LLM 输出的权限/安全/合规相关字段。LLM 的角色是内容顾问——它可以建议mainSubject、tone、style但它无权决定是否允许文字、是否允许外链、内容是否安全。哪些字段必须代码覆写字段类别示例为什么不能信任 LLM权限字段allowText,canMentionBrands,isPublic安全策略不容 LLM 决定长度/数量限制maxWords,imageCount成本控制且 LLM 不擅长数字内容安全判断是否包含敏感话题、违规内容LLM 的自评不可靠——它经常判断错误格式字段输出格式是否匹配预期 schema代码比 LLM 更擅长格式校验业务规则是否符合年龄分级、地区限制业务逻辑应集中管理不能散落在 LLM 判断中代码示例# ❌ 危险信任 LLM 的安全自评 response llm.chat_json(Generate content and mark it as safe if appropriate.) if response[is_safe]: # ← 不能相信这个值 publish(response[content]) # ✅ 正确独立的安全检查 代码覆写 response llm.chat_json(Generate content about: topic) plan response # 代码覆写——不管 LLM 说了什么 plan[allow_external_links] False # 硬编码安全策略 plan[max_output_tokens] 500 # 硬编码成本控制 plan[content] safety_filter(plan[content]) # 独立安全检查 plan[flags] run_content_classifier(plan[content]) if all_checks_pass(plan): publish(plan[content])核心心态LLM 的输出 建议 代码的覆写 裁决 建议可以被采纳但裁决不容商量。7. 原则六输入分类 模板路由问题用一个万能 prompt 处理所有类型的输入必然在边缘情况翻车。不同类型的输入需要不同的指令策略。模式输入 → 分类器(纯代码) → 路由到对应模板 → 组装 prompt分类器必须用纯代码正则、关键词、长度判断不用 LLM零延迟、零成本确定性——不会分错类可以记录分到哪个类用于后续效果分析和 A/B 测试分类维度设计示例业务场景分类维度分类方法客服机器人意图投诉 / 咨询 / 售后关键词 正则内容生成类型短文本 / 长文 / 代码 / 表格长度 特征检测翻译语言对 领域技术 / 文学 / 口语语言检测 术语库匹配图片生成语义粒度字母 / 单词 / 短语 / 句子 / 抽象单词计数 标点检测代码审查语言 变更类型新功能 / Bug修复 / 重构文件扩展名 diff 分析代码示例def classify_input(text: str) - str: 纯代码分类零 LLM 调用 text text.strip() # 按优先级从特殊到一般 if re.match(r^[A-Za-z]$, text): return single_letter word_count len(text.split()) has_punctuation bool(re.search(r[!?.], text)) if word_count 4 or has_punctuation: return sentence if word_count 2: return phrase if re.match(r^[A-Za-z]$, text): return word return abstract # 兜底 # 每类路由到不同的 builder TEMPLATES { single_letter: build_letter_prompt, sentence: build_sentence_prompt, phrase: build_phrase_prompt, word: build_word_prompt, abstract: build_abstract_prompt, } def process(input_text: str) - str: category classify_input(input_text) plan llm_plan(input_text, category) # LLM 知道分类可针对性规划 return TEMPLATES[category](plan) # 选择对应模板组装8. 原则七约束排序与密度控制核心洞察LLM 对 prompt 前半部分的关注度显著高于后半部分。最重要的约束应该出现在前 25% 的位置。排序策略┌─────────────────────────────────────────┐ │ 1. 角色 / 格式硬约束最前最高权重 │ ← 不可违反的规则 │ 2. 核心任务描述正向引导 │ ← 告诉 LLM 要做什么 │ 3. 风格 / 语气要求 │ ← 质量要求 │ 4. 条件性指令按需 │ ← 场景特化 │ 5. 负向约束 / 禁止列表靠后 │ ← 安全网不要放在最前面 │ 6. 用户输入数据最后 │ ← 避免被 LLM 误解释为指令 └─────────────────────────────────────────┘约束密度的陷阱堆砌大量负向约束是最常见的错误之一❌ 过度密集的负向约束 不要做A。不要做B。不要做C。不要做D。不要做E。不要做F。不要做G。不要做H。 不要做I。不要做J。不要做K。不要做L。不要做M。不要做N。不要做O。不要做P。两个问题认知稀释约束太多每条都不突出LLM 倾向于全部忽略注意力劫持LLM 在处理长否定列表时消耗大量注意力正向引导被边缘化改进策略# ❌ 20 条零散的负向约束 negatives [Do not do A., Do not do B., ..., Do not do T.] # ✅ 精简为 3-5 条分组约束 constraints [ # 分组 1: 格式 Output must be valid JSON with exactly these fields: [...], # 分组 2: 内容安全合并同类项 Do not include personal opinions, speculation, unverified claims, or political commentary., # 分组 3: 风格 Maintain a neutral, factual tone. Use professional language only., ]约束设计经验法则约束类型建议数量表示方式硬约束不可违反1-3 条放在最前面每条单独一行正向引导2-5 条放在中间描述要做什么负向禁止3-6 条同类分组放在靠后同类合并为一句话示例few-shot1-3 个质量 数量9. 原则八防御性降级问题任何依赖 LLM 的系统都必须面对一个事实LLM 调用可能失败超时、限流、格式错误、返回空内容、模型不可用。系统不应因 LLM 故障而完全不可用。降级策略层次优先级策略适用场景1重试exponential backoff jitter临时性故障限流、网络抖动2使用缓存结果相同输入的之前成功结果幂等操作、重复请求3降级到更小/更快的模型如 GPT-4 → GPT-4o-mini主模型不可用或超预算4使用规则引擎替代 LLM模板 关键词匹配LLM 完全不可用5返回安全的默认值最终兜底6返回错误并记录比返回错误结果更安全无可用降级路径时代码示例async def generate_with_fallback(user_input: str) - str: try: # 主路径完整 Plan Build plan await llm_plan(user_input) return build_prompt(plan) except LLMError as e: logger.warning(fLLM plan failed: {e}, degrading to fallback) # Fallback: 跳过 Plan 阶段直接用简化模板 # 不依赖 LLM仍能返回可用的 prompt category classify_input(user_input) return build_simple_prompt(user_input, category)关键原则Fallback 路径必须极简化——只做必要的最小处理不要再引入复杂的 LLM 调用链记录每次降级——用于监控 LLM 服务质量和优化重试策略不要让降级本身成为新的故障点——Fallback 逻辑应尽量是纯代码10. 原则九可观测性设计问题Prompt 系统上线后你如何知道它在想什么哪个阶段出了问题模式在每个阶段边界打结构化 Log# 每个阶段记录输入和输出 logger.info([Stage:Parse], extra{ trace_id: trace_id, input_snippet: raw_input[:100], output: json.dumps(parsed) }) logger.info([Stage:Plan], extra{ trace_id: trace_id, input: json.dumps(plan_input), output: json.dumps(plan), # ← 最关键查看 LLM 的规划 model: gpt-4o-mini, latency_ms: elapsed_ms, }) logger.info([Stage:Build], extra{ trace_id: trace_id, category: category, final_prompt: final_prompt, # ← 最关键查看最终发给模型的 prompt }) logger.info([Stage:Execute], extra{ trace_id: trace_id, prompt_length: len(final_prompt), model: dashscope, latency_ms: elapsed_ms, })为什么 Plan 和 Final Prompt 的 Log 最关键你看到的能判断什么Plan 正确Final Prompt 有问题Bug 在build_prompt()代码逻辑Plan 错误Final Prompt 跟着错Bug 在 LLM Planner 的 system prompt 或 payloadPlan 为空 / 格式错误LLM 调用失败——检查网络、限流、模型可用性Plan 和 Final Prompt 都正确最终效果差问题在目标模型侧不是 prompt 问题结构化日志的建议字段log_entry { trace_id: str(uuid.uuid4()), # 全链路追踪 stage: plan | build | execute, category: word, # 分类结果 input_snippet: ..., # 截断后的输入 output_snippet: ..., # 截断后的输出 model: gpt-5.4-mini, # 使用的模型 latency_ms: 342, # 耗时 token_usage: {input: 120, output: 45}, error: None, # 错误信息如有 }附录本文档的设计模式和原则提炼自生产环境中经过验证的 Prompt 工程实践适用于但不限于以下场景文生图 / 文生视频 Prompt 工程多阶段 Plan-Build 流水线精确控制视觉输出对话系统 (Chatbot)意图分类 模板路由 上下文管理 安全降级内容审核与安全Schema 约束 代码覆写 独立安全检查层RAG 系统Query 改写 → 检索 → 重排序 → 生成每个阶段可独立优化Agent / Tool CallingPlan 阶段决定调用哪些工具Build 阶段组装工具参数代码生成分类代码类型 → Plan 架构 → Build 代码结构 → 生成