
结构化 Prompt 模板:让 AI 输出稳定可控适用读者:AI 应用开发者、Prompt 工程师技术栈:Python 3.10 / LangChain / Pydantic / Jinja2阅读时长:约 12 分钟一、为什么需要结构化 Prompt?很多人写 Prompt 是这样的:帮我写一个产品介绍,关于我们公司的 AI 客服,要专业一点,不要太长。然后 AI 给的答案飘忽不定——这次写 200 字,下次写 800 字;这次严肃,下次活泼;有时还漏掉关键信息。根因:Prompt 缺少结构。LLM 只能猜你想要什么。结构化 Prompt 的核心思想:把模糊的帮我写一下变成明确的任务说明书让 LLM 知道:角色、任务、输入、输出、约束、示例用模板固化优秀 Prompt,变量动态注入,既灵活又稳定二、CRISPE 框架:5 段式万能模板CRISPE 是业界最常用的结构化 Prompt 框架,对应 5 个段:段含义作用Capacity角色让 LLM 代入专家身份Request任务明确要做什么Insight背景提供必要上下文Style风格控制语气和表达方式Purpose目标明确要达成什么Extra额外要求格式、长度、禁止项示例:产品介绍 Prompt# Capacity(角色) 你是一位资深的产品营销文案,擅长用简洁有力的语言打动 B 端客户。 # Request(任务) 请为我们的产品「智能客服系统」撰写一段 200 字的产品介绍。 # Insight(背景) - 产品名称:智言客服 - 核心功能:基于大模型的 AI 客服,7×24 小时自动回复 - 目标用户:电商、金融、教育的客服团队 - 差异化优势:支持私有化部署,准确率达 95% # Style(风格) - 语气专业、有说服力 - 多用短句,避免堆砌专业术语 - 突出商业价值,而非技术细节 # Purpose(目标) 让企业 CTO/客服负责人读完愿意预约演示。 # Extra(额外要求) - 输出为 Markdown 格式 - 包含 1 个加粗的核心卖点 - 末尾用一句话总结价值主张效果对比:同样的产品,普通 Prompt 输出参差不齐,CRISPE 输出稳定、专业、可用。三、用代码管理 Prompt 模板把 Prompt 写在代码里用f-string?改起来到处找。用Jinja2 模板YAML 配置,更专业。1. Prompt 配置文件(Prompts YAML)# prompts/product_intro.yaml# 产品介绍 Prompt 模板template_id:product_intro_v1version:1.0description:B 端产品介绍生成器,输出 Markdowntemplate:|# 角色 你是{{ role }}。# 任务请为产品「{{product_name}}」撰写一段{{length}}字的产品介绍。# 背景{% for item in background %}-{{item}}{% endfor %}# 风格{{style}}# 输出格式-Markdown 格式-突出{{highlight_count}}个核心卖点-末尾用一句话总结价值主张variables:role:type:stringdefault:资深的产品营销文案product_name:type:stringrequired:truelength:type:intdefault:200background:type:listdefault:[]style:type:stringdefault:专业、有说服力,多用短句highlight_count:type:intdefault:32. 模板渲染器# prompt_renderer.py# 通用 Prompt 模板渲染器fromjinja2importTemplateimportyamlfrompathlibimportPathfromtypingimportAnyclassPromptRenderer:# 把 YAML 中的 Jinja2 模板 业务变量 - 渲染后的 Prompt 字符串# 优势:# 1. Prompt 与代码分离,非工程师也能改# 2. 同一模板可复用,只换变量# 3. 模板版本化管理,方便 A/B 测试def__init__(self,prompts_dir:str./prompts):# prompts_dir: 存放 YAML 模板的目录self.prompts_dirPath(prompts_dir)self.cache{}# 简单缓存,避免重复读文件defload(self,template_id:str)-dict:# 从 YAML 文件加载模板(带缓存)iftemplate_idinself.cache:returnself.cache[template_id]pathself.prompts_dir/f{template_id}.yamlwithopen(path,encodingutf-8)asf:datayaml.safe_load(f)self.cache[template_id]datareturndatadefrender(self,template_id:str,variables:dict[str,Any])-str:# 渲染模板# 1. 加载模板定义template_defself.load(template_id)# 2. 合并默认变量和用户传入变量merged{}forvar_name,var_confintemplate_def.get(variables,{}).items():# 优先用用户传入的,否则用默认值merged[var_name]variables.get(var_name,var_conf.get(default))# 必填校验if(var_conf.get(required)andmerged[var_name]isNone):raiseValueError(f变量{var_name}必填)# 3. 用 Jinja2 渲染templateTemplate(template_def[template])returntemplate.render(**merged)# 使用示例if__name____main__:rendererPromptRenderer(prompts_dir./prompts)promptrenderer.render(product_intro_v1,{product_name:智言客服,background:[基于大模型的 AI 客服,7×24 小时自动回复,准确率达 95%,],length:250,},)print(prompt)四、控制输出格式:JSON Schema 强约束LLM 输出不稳定?用 JSON Schema 强制约束。# structured_output.py# 用 Pydantic LangChain 强制 LLM 输出结构化数据fromtypingimportListfrompydanticimportBaseModel,Fieldfromlangchain_openaiimportChatOpenAIfromlangchain.output_parsersimportPydanticOutputParser# 1. 用 Pydantic 定义输出结构classProductIntro(BaseModel):# 产品名称title:strField(description产品名称)# 卖点列表highlights:List[str]Field(description3 个核心卖点,每个不超过 20 字)# 一句话价值主张tagline:strField(description一句话总结,不超过 30 字)# 字数word_count:intField(description实际字数,用于校验)# 2. 创建输出解析器parserPydanticOutputParser(pydantic_objectProductIntro)# 解析器会自动生成 JSON Schema 指令format_instructionsparser.get_format_instructions()# 3. 构造 PromptPRODUCT_PROMPT 你是资深产品营销文案。请根据以下信息生成产品介绍。 产品名称:{product_name} 核心功能:{features} 目标用户:{target_users} {format_instructions} defgenerate_product_intro(product_name:str,features:str,target_users:str,)-ProductIntro:# 4. 调用 LLM 并解析结果llmChatOpenAI(modelgpt-4o-mini,temperature0)# 5. 把 format_instructions 注入 PromptpromptPRODUCT_PROMPT.format(product_nameproduct_name,featuresfeatures,target_userstarget_users,format_instructionsformat_instructions,)# 6. invoke parse:LLM 输出的字符串被自动解析为 Pydantic 对象responsellm.invoke(prompt)resultparser.parse(response.content)# 7. 业务校验ifresult.word_count300:print(f警告:字数{result.word_count}超过 300)returnresult# 调用示例if__name____main__:introgenerate_product_intro(product_name智言客服,featuresAI 自动回复,准确率 95%,target_users电商客服团队,)print(intro.title)print(intro.highlights)print(intro.tagline)效果:LLM 100% 输出合规 JSON,不再需要写复杂的正则解析或重试。五、Few-shot:用示例教 LLM 学格式LLM 看到示例会比指令学得更快。# few_shot_prompt.py# Few-shot:在 Prompt 中给出 1~3 个示例,让 LLM 模仿fromlangchain.promptsimportFewShotPromptTemplate,PromptTemplate# 1. 定义示例(从历史数据中挑典型的好答案)EXAMPLES[{review:这个 AI 客服回答慢,经常答非所问,sentiment:负面,category:功能问题,action:排查知识库匹配,优化 RAG 召回,},{review:界面挺好看的,操作也简单,sentiment:正面,category:UI 体验,action:无需处理,记录为优势,},{review:能不能加一个导出 Excel 的功能?,sentiment:中性,category:功能建议,action:记录到需求池,优先级 P2,},]# 2. 单个示例的模板example_templatePromptTemplate(input_variables[review,sentiment,category,action],template(评价:{review}\n情感:{sentiment}\n类别:{category}\n建议:{action}),)# 3. Few-shot 模板few_shot_promptFewShotPromptTemplate(examplesEXAMPLES,# 示例列表example_promptexample_template,prefix(你是用户反馈分析助手。请按以下格式分析用户评价(情感 / 类别 / 建议处理动作):),suffix评价:{review}\n情感:\n类别:\n建议:,input_variables[review],)# 使用defanalyze_review(review:str)-str:fromlangchain_openaiimportChatOpenAI llmChatOpenAI(modelgpt-4o-mini,temperature0)final_promptfew_shot_prompt.format(reviewreview)returnllm.invoke(final_prompt).content对比:方式准确率备注0-shot(只给指令)75%类别经常分错3-shot(给 3 个示例)93%显著提升六、Chain-of-Thought:让 LLM 思考后再答复杂任务,直接要答案容易出错。让 LLM一步步思考。# cot_prompt.py# Chain-of-Thought:在 Prompt 中显式要求先分析,再回答COT_TEMPLATE 你是数据分析助手。请基于以下数据回答问题。 数据: {context} 问题:{question} 请按以下步骤回答: 1. 先列出已知信息(从数据中提取) 2. 再分析可能的推论 3. 最后给出结论 回答: # Few-shot CoT 效果更好:给出思考过程的示例COT_WITH_EXAMPLES 你是数学老师。请解答应用题。 例题:小明有 5 个苹果,吃了 2 个,又买了 3 个,现在有几个? 解答步骤: 1. 已知:小明原有 5 个苹果 2. 吃了 2 个:5 - 2 3 个 3. 又买了 3 个:3 3 6 个 4. 结论:小明现在有 6 个苹果 题目:{question} 解答步骤: defsolve_with_cot(question:str)-str:fromlangchain_openaiimportChatOpenAI llmChatOpenAI(modelgpt-4o-mini,temperature0)promptCOT_WITH_EXAMPLES.format(questionquestion)returnllm.invoke(prompt).content适用场景:数学、推理、复杂分析、需要多步逻辑的任务。七、实战案例库案例 1:工单分类(结构化输出)# case_classifier.py# 用 Pydantic 强约束工单分类输出frompydanticimportBaseModel,FieldfromtypingimportLiteralfromlangchain_openaiimportChatOpenAIfromlangchain.output_parsersimportPydanticOutputParserclassTicketCategory(BaseModel):# 用 Literal 限定枚举值,LLM 只能从这里选category:Literal[技术问题,账单问题,功能建议,其他](Field(description工单类别,4 选 1))priority:Literal[P0,P1,P2,P3](Field(description优先级,P0 最高))summary:strField(description一句话总结,不超过 30 字)parserPydanticOutputParser(pydantic_objectTicketCategory)llmChatOpenAI(modelgpt-4o-mini,temperature0)defclassify_ticket(content:str)-TicketCategory:prompt(请对以下工单进行分类。\nf{parser.get_format_instructions()}\n\nf工单内容:{content})returnparser.parse(llm.invoke(prompt).content)案例 2:数据提取(从非结构化文本提取字段)# case_extractor.py# 从合同文本中提取关键字段classContractInfo(BaseModel):party_a:strField(description甲方名称)party_b:strField(description乙方名称)amount:floatField(description合同金额(元))start_date:strField(description开始日期,YYYY-MM-DD)end_date:strField(description结束日期,YYYY-MM-DD)defextract_contract_info(text:str)-ContractInfo:parserPydanticOutputParser(pydantic_objectContractInfo)llmChatOpenAI(modelgpt-4o-mini,temperature0)prompt(请从以下合同文本中提取关键信息。\nf{parser.get_format_instructions()}\n\nf合同文本:{text})returnparser.parse(llm.invoke(prompt).content)案例 3:内容改写(风格控制)# case_rewriter.py# 把口语化评价改写为专业客服回复REWRITE_TEMPLATE 你是客服话术专家。请把用户的口语化反馈改写为专业、礼貌的客服回复。 要求: 1. 不改变原意 2. 第一人称我,称呼用户您 3. 表达同理心 给出解决方案 4. 不超过 100 字 用户反馈:{feedback} 客服回复: defrewrite_feedback(feedback:str)-str:fromlangchain_openaiimportChatOpenAI llmChatOpenAI(modelgpt-4o-mini,temperature0.3)promptREWRITE_TEMPLATE.format(feedbackfeedback)returnllm.invoke(prompt).content八、Prompt 评估与 A/B 测试Prompt 优化靠猜?不,靠数据。# prompt_ab_test.py# 同一任务,两个 Prompt 模板,看哪个效果好fromtypingimportListfromdataclassesimportdataclassdataclassclassPromptVariant:# 一个 Prompt 变体name:strtemplate:str# 评估结果scores:List[float]Nonedef__post_init__(self):ifself.scoresisNone:self.scores[]classPromptABTester:# Prompt A/B 测试框架# 用法:# 1. 准备 N 条测试数据(输入 期望输出)# 2. 用不同 Prompt 跑同一批数据# 3. 用评估函数(相似度、人工评分)打分# 4. 选分数最高的 Prompt 上线def__init__(self,eval_dataset:List[dict]):# eval_dataset: [{input: ..., expected: ...}, ...]self.eval_dataseteval_dataset self.variants:List[PromptVariant][]defadd_variant(self,name:str,template:str):# 注册一个 Prompt 变体self.variants.append(PromptVariant(namename,templatetemplate))defrun(self,llm,score_fn)-dict:# 跑测试,返回每个变体的平均分results{}forvariantinself.variants:forsampleinself.eval_dataset:# 1. 用该变体生成输出promptvariant.template.format(**sample[input])outputllm.invoke(prompt).content# 2. 算分(score_fn 是输出 vs 期望的相似度)scorescore_fn(output,sample[expected])variant.scores.append(score)# 取平均分avgsum(variant.scores)/len(variant.scores)results[variant.name]avgreturnresults# 使用示例defsimilarity_score(output:str,expected:str)-float:# 简单相似度:可以用 embedding cosine / 编辑距离 / LLM-as-judgefromdifflibimportSequenceMatcherreturnSequenceMatcher(None,output,expected).ratio()# 准备 20 条测试数据dataset[{input:{topic:AI 客服},expected:智能、高效、24h在线...},# ... 共 20 条]testerPromptABTester(dataset)tester.add_variant(v1_simple,请写一段关于 {topic} 的介绍)tester.add_variant(v2_structured,你是营销专家...{topic}...)# from langchain_openai import ChatOpenAI# results tester.run(ChatOpenAI(...), similarity_score)# print(results)# 输出:{v1_simple: 0.62, v2_structured: 0.81} # v2 胜出九、5 个常见踩坑坑 1:Prompt 太长,LLM 抓不到重点症状:Prompt 写了 2000 字,LLM 反而答得不好。解决:重要信息放最前面(lost in the middle 现象),或用important标签包裹关键指令。坑 2:示例不够典型症状:给了 3 个 Few-shot,LLM 还是学不会。解决:示例要多样:覆盖边界情况示例要正确:别用反例数量 3~5 个最佳,太多反而稀释坑 3:温度参数没设对症状:事实型问题答案飘忽,创意任务不够发散。解决:事实型(分类、提取、JSON 输出):temperature0创意型(写文案、brainstorm):temperature0.7~1.0坑 4:Prompt 写死在代码里症状:想改一个用词,得发版。解决:Prompt 拆到 YAML/数据库,运行时加载(见上文prompt_renderer.py)。坑 5:不写版本号症状:Prompt 改坏了,不知道回滚到哪个版本。解决:YAML 模板加version字段用 Git 管理 Prompt 文件上线前 A/B 测试(见上文)十、模板速查表场景推荐框架关键参数通用任务CRISPE 5 段式temperature0JSON 输出Pydantic format_instructionstemperature0文本分类Few-shot 3~5 例temperature0复杂推理Chain-of-Thoughttemperature0创意写作CRISPE 风格示例temperature0.7数据提取Pydantic Schematemperature0多轮对话角色 历史 任务temperature0.3翻译润色直接指令 Few-shottemperature0.2十一、总结结构化 Prompt 的本质,是把和 AI 聊天变成和 AI 写需求文档。CRISPE 5 段式:通用任务的标准模板YAML Jinja2:把 Prompt 变成可维护的工程资源Pydantic Schema:强约束输出,告别解析噩梦Few-shot / CoT:用示例和推理链弥补指令的不足A/B 测试:用数据说话,持续优化掌握这套方法,你会发现 LLM 输出从看运气变成按预期,从此告别改 10 次才出 1 个能用的尴尬。参考文档LangChain Prompt Templates:https://python.langchain.com/docs/modules/model_io/prompts/Pydantic Output Parser:https://python.langchain.com/docs/modules/model_io/output_parsers/pydanticCRISPE 框架:Capacity / Request / Insight / Style / Purpose / Extra