Prompt 管理怎么做?不要把 Prompt 散落在业务代码里
上一篇文章讨论了 LLM 抽象层。LLM 抽象层解决的是模型调用问题。它让业务代码不直接依赖 OpenAI、DeepSeek、Qwen 等具体模型供应商。但在真实 AI 应用里模型调用只是其中一部分。还有一个更容易被忽视的问题Prompt 应该怎么管理很多 AI Demo 会这样写defsummarize(text:str)-str:promptf 请总结下面这段内容要求简洁、准确{text}returnllm.chat([{role:user,content:prompt}]).content这段代码很容易理解。如果项目只有一个功能这样写没有太大问题。但如果项目继续发展问题很快就会出现。总结文章需要 Prompt。提取关键词需要 Prompt。生成标题需要 Prompt。分析用户意图需要 Prompt。调用工具前需要 Prompt。Agent 规划任务需要 Prompt。RAG 生成回答需要 Prompt。如果这些 Prompt 都写在不同函数里项目会越来越难维护。所以AI 应用工程化不能只管理模型调用也要管理 Prompt。一、为什么 Prompt 不应该散落在业务代码里Prompt 散落在业务代码里短期看起来很方便。因为开发者可以在写逻辑时顺手把提示词写进去。但这种方式至少有几个问题。第一难以修改。当 Prompt 写在多个函数、多个模块、多个类里时想统一调整风格或输出格式需要到处搜索和修改。第二难以复用。同一个 Prompt 可能被多个功能使用。如果每个地方都复制一份后续很容易出现版本不一致。第三难以测试。Prompt 的变化会影响模型输出。如果 Prompt 和业务代码混在一起就很难单独测试 Prompt 效果。第四难以追踪。线上出现一次异常输出时开发者需要知道当时使用的是哪个 Prompt、哪个版本、哪些变量、哪个模型。如果没有统一管理就很难定位问题。第五职责不清。业务代码应该负责业务流程。LLM 层应该负责模型调用。Prompt 层应该负责构造模型输入。如果三者混在一起项目边界会越来越模糊。所以Prompt 不应该只是散落在代码里的字符串。它应该被当成 AI 应用的重要资产来管理。二、Prompt 是 AI 应用的业务资产很多人会把 Prompt 看成一段普通文本。但在 AI 应用里Prompt 往往承载了业务规则。例如你是一名严谨的技术编辑。 请根据用户提供的文章内容生成摘要。 摘要要求 1. 不超过 200 字。 2. 保留核心技术观点。 3. 不添加原文没有的信息。 4. 使用中文输出。这段 Prompt 里面包含了角色、任务、输出长度、事实约束和语言要求。它不是普通字符串。它决定了模型应该如何理解任务。它决定了模型输出应该遵守什么规则。它也决定了业务系统能否稳定得到可用结果。如果 Prompt 写得不好模型可能会输出格式不稳定。生成与业务无关的内容。添加不存在的信息。忽略字段限制。返回无法被程序解析的文本。所以Prompt 应该像配置、接口和数据结构一样被认真管理。尤其在 AI Scaffold 这样的脚手架项目里Prompt 管理不是锦上添花而是基础能力。三、Prompt 管理层应该负责什么Prompt 管理层不是简单地把字符串放到文件里。一个合理的 Prompt 管理层至少应该负责几个事情。第一集中存放 Prompt。所有 Prompt 应该有明确位置而不是散落在业务代码里。第二支持模板变量。Prompt 通常需要注入文章内容、用户问题、上下文、工具结果等变量。第三支持版本管理。同一个任务的 Prompt 可能会迭代多个版本。第四支持不同输出格式。有些 Prompt 需要返回自然语言有些需要返回 JSON有些需要返回分类标签。第五支持调试记录。至少要能记录最终渲染出的 Prompt、变量、模型、返回结果和执行时间。第六和 LLM 抽象层解耦。Prompt 管理层负责生成模型输入。LLM 抽象层负责调用模型。这两层不应该互相混淆。换句话说Prompt 管理层的目标是让 Prompt 可维护、可复用、可追踪。四、Prompt 文件应该放在哪里在 AI 应用项目里可以单独建立prompts/目录。例如prompts/ ├── summary/ │ ├── v1.md │ └── v2.md ├── title/ │ └── v1.md ├── intent/ │ └── v1.md └── rag/ └── answer_v1.md这种结构有几个好处。第一Prompt 位置清晰。开发者一看目录就知道项目里有哪些 AI 任务。第二支持任务分类。不同业务场景的 Prompt 可以放在不同目录。第三支持版本演进。同一个任务可以保留v1.md、v2.md、v3.md。第四方便代码加载。业务代码只需要通过 Prompt 名称和版本加载模板。例如promptprompt_manager.render(namesummary,versionv1,variables{text:article_text},)这样 Prompt 不再散落在函数里而是成为项目中可以被管理的资源。五、一个 Prompt 模板可以怎么写Prompt 模板的重点是把固定规则和动态变量分开。例如prompts/summary/v1.md可以这样写你是一名严谨的技术内容编辑。 请根据下面的文章内容生成一段摘要。 要求 - 使用中文输出。 - 不超过 200 字。 - 保留文章的核心技术观点。 - 不添加文章中没有出现的信息。 - 不使用夸张营销语气。 文章内容 {{ text }}这里的{{ text }}就是变量。模板本身定义任务规则。业务代码传入具体文章内容。渲染以后才得到最终发送给模型的 Prompt。这样做的好处是Prompt 规则可以独立修改。业务代码不需要关心 Prompt 细节。同一个 Prompt 可以被不同 Workflow 复用。模板变量也可以被检查和记录。这比在函数里写大段三引号字符串更清晰。六、Prompt 渲染应该简单可靠Prompt 管理层可以先实现一个简单的渲染函数。例如frompathlibimportPathclassPromptManager:def__init__(self,base_dir:strprompts):self.base_dirPath(base_dir)defload(self,name:str,version:strv1)-str:pathself.base_dir/name/f{version}.mdreturnpath.read_text(encodingutf-8)defrender(self,name:str,variables:dict,version:strv1)-str:templateself.load(namename,versionversion)forkey,valueinvariables.items():templatetemplate.replace({{ key }},str(value))returntemplate这个实现很简单。它适合脚手架早期版本。真实项目里可以继续增强检查变量是否缺失。支持默认值。支持更严格的模板语法。支持模板缓存。支持 Prompt 元数据。但一开始不一定要做得很复杂。先把 Prompt 从业务代码里抽出来就已经解决了很大一部分维护问题。七、Prompt 变量需要被校验Prompt 模板里使用变量就一定要考虑变量是否完整。例如模板里写了用户问题 {{ question }} 检索上下文 {{ context }}如果业务代码只传了question没有传context最终 Prompt 可能会带着未替换的占位符进入模型。这会导致模型输出异常。所以 PromptManager 应该能检查变量。例如importredefextract_variables(template:str)-set[str]:returnset(re.findall(r{{\s*(\w)\s*}},template))defvalidate_variables(template:str,variables:dict)-None:requiredextract_variables(template)missingrequired-set(variables.keys())ifmissing:raiseValueError(fMissing prompt variables:{missing})这样在调用模型之前就能发现 Prompt 变量缺失。错误越早暴露越容易修复。这和配置系统里的启动时校验是同一个思路。八、Prompt 和 Messages 的关系很多模型使用的是 messages 格式。例如messages[{role:system,content:你是一名严谨的技术编辑。},{role:user,content:请总结下面这段文章...},]所以 Prompt 管理不一定只管理一个字符串。它也可以管理 messages 模板。例如prompts/summary/v1.json[{role:system,content:你是一名严谨的技术内容编辑。},{role:user,content:请总结下面的文章内容要求不超过 200 字\n\n{{ text }}}]这样 PromptManager 渲染后可以直接返回 messages。messagesprompt_manager.render_messages(namesummary,versionv1,variables{text:article_text},)resultllm.chat(messages)这种方式和 LLM 抽象层配合得更自然。Prompt 层负责生成 messages。LLM 层负责发送 messages。业务层负责决定使用哪个 Prompt。九、Prompt 不应该包含模型调用逻辑Prompt 管理层要保持边界。它不应该直接调用模型。例如不要设计成这样summaryprompt_manager.run(summary,{text:text})这个run如果内部直接调用 LLM就会让 Prompt 层和模型调用层混在一起。更清晰的写法是messagesprompt_manager.render_messages(namesummary,versionv1,variables{text:text},)resultllm.chat(messages)这样每一层的职责都很明确。PromptManager 负责模板渲染。LLMClient 负责模型调用。Workflow 负责业务流程编排。如果未来要替换模型供应商只改 LLM 层。如果未来要优化提示词只改 Prompt 文件。如果未来要调整业务步骤只改 Workflow。这就是工程化分层的意义。十、Prompt 版本管理很重要Prompt 经常会迭代。比如摘要 Prompt 的第一版可能只是请总结下面的文章 {{ text }}后来发现输出太长于是增加长度限制。再后来发现模型会补充原文没有的信息于是增加事实约束。再后来需要适配 CSDN 摘要于是增加语气和字数要求。如果每次都直接覆盖原文件就会有几个问题。第一不知道线上使用过哪个版本。第二出现效果下降时无法回滚。第三不方便对比不同版本的输出效果。所以可以保留版本prompts/summary/ ├── v1.md ├── v2.md └── v3.md配置里可以指定默认版本SUMMARY_PROMPT_VERSIONv2业务代码通过配置选择版本。这样 Prompt 迭代就更可控。十一、Prompt 调试需要记录什么AI 应用出现问题时经常需要回看当时的输入输出。Prompt 调试记录至少应该包含Prompt 名称。Prompt 版本。渲染后的最终 Prompt。输入变量。使用的模型。模型返回结果。执行耗时。是否成功。错误信息。例如可以记录成这样{prompt_name:summary,prompt_version:v2,model:gpt-4o,success:true,latency_ms:1530}真实项目里不一定要把完整 Prompt 和完整输出都打到普通日志里。因为里面可能包含用户输入或敏感信息。但至少要有可追踪能力。开发环境可以记录更完整。生产环境可以做脱敏和采样。没有记录就很难持续优化 Prompt。十二、Prompt 测试不是追求百分百确定模型输出本身有不确定性。所以 Prompt 测试不能完全像普通函数测试一样只判断输出是否等于某个固定字符串。但仍然可以做一些测试。例如测试模板变量是否完整deftest_summary_prompt_variables():templateprompt_manager.load(summary,v1)requiredextract_variables(template)asserttextinrequired测试渲染结果是否不再包含占位符deftest_summary_prompt_render():promptprompt_manager.render(namesummary,versionv1,variables{text:测试文章内容},)assert{{notinpromptassert测试文章内容inprompt测试输出结构是否满足要求deftest_json_prompt_output_schema():resultrun_with_fake_llm()asserttitleinresultassertsummaryinresult这些测试不能保证模型永远输出完美结果。但它们可以保证 Prompt 模板本身没有明显工程错误。这对项目稳定性很有帮助。十三、Prompt 管理在 AI Scaffold 里的位置在 AI Scaffold 里Prompt 管理可以作为一个独立模块。项目结构可以这样安排app/ ├── main.py ├── config/ │ └── settings.py ├── llm/ │ ├── base.py │ ├── factory.py │ └── providers/ ├── prompts/ │ ├── summary/ │ │ └── v1.md │ ├── title/ │ │ └── v1.md │ └── intent/ │ └── v1.md ├── prompt_manager/ │ ├── __init__.py │ ├── manager.py │ └── validators.py └── workflows/ └── summary_workflow.pyCLI 创建项目时可以自动生成prompts/ prompt_manager/并提供一个默认示例prompts/summary/v1.md这样开发者创建项目后就会自然形成一个习惯Prompt 放在prompts/目录里。Prompt 渲染通过PromptManager。模型调用通过LLMClient。业务流程通过Workflow。这个习惯比单纯生成代码模板更重要。因为脚手架不仅是在创建文件也是在帮助项目建立工程边界。十四、总结Prompt 不应该只是写在函数里的三引号字符串。在 AI 应用里Prompt 承载了任务规则、输出约束、业务风格和模型交互方式。如果 Prompt 散落在业务代码里项目会很难维护、复用、测试和追踪。一个合理的 Prompt 管理方案至少应该做到Prompt 集中放在prompts/目录。使用模板变量注入动态内容。对变量缺失进行校验。支持字符串 Prompt 和 messages Prompt。Prompt 层只负责构造输入不负责调用模型。同一任务可以保留多个 Prompt 版本。调试时能记录 Prompt 名称、版本、变量、模型和输出。测试时能检查模板变量、渲染结果和输出结构。对于 AI Scaffold 来说配置系统决定运行参数。LLM 抽象层负责模型调用。Prompt 管理层负责构造高质量、可维护、可追踪的模型输入。这三层结合起来AI 应用才不会停留在 Demo 阶段。下一篇文章可以继续讨论 Workflow当一个 AI 任务不再是一次模型调用而是多个步骤、多个节点、多个条件分支时应该如何用工作流来组织整个执行过程。