Prompt 是一种新型契约,越像规范,输出越稳
Prompt 不是聊天,是写规范
很多新手对 Prompt 的第一印象是"我跟 AI 聊几句它就懂了"。这种聊天式的用法做娱乐性 demo 没问题,但只要你想把 LLM 嵌进任何严肃产品流程里,第一件事就要做思维转换:Prompt 不是聊天稿,而是一份给模型看的需求规范。规范越清晰、约束越明确,输出就越稳定可控。
可以用 Python 的视角类比:传统函数靠类型签名和 docstring 来约束行为,调用者只能按签名传参;而 Prompt 是用自然语言写的"函数签名",模型按它的理解去执行。两者的 API 契约位置是对应的——只是表达方式从代码变成了自然语言。
# 传统函数:契约写在签名里
def classify_email(content: str) -> Literal["spam", "normal"]:
"""判断邮件是否为垃圾邮件。"""
...
# Prompt:契约写在文本里
prompt = """\
你是一个邮件分类器。
任务:判断给定邮件正文是垃圾邮件还是正常邮件。
输出:只返回 "spam" 或 "normal",不要任何解释或额外文字。
邮件正文:
{content}
"""
理解到这一层,你就能开始系统化地写 Prompt,而不是反复"再试一种问法"。
一份合格 Prompt 的四件套
把任意一个工业级 Prompt 拆开看,几乎都包含下面四个部分。哪怕只是一段简单分类任务,把这四块写齐,效果会比拍脑袋的版本稳定得多。
1. 角色(Role)——把模型置入一个具体场景。这一步看起来玄学,但有实证效果:被设定成"资深 Python 工程师"的模型在写代码时确实比"AI 助手"更接近你想要的风格。
2. 任务(Task)——明确说出"你要做什么",一句话写清核心动作。避免"帮我看看""分析一下"这种模糊词。
3. 约束(Constraints)——明确说出"不能做什么 / 要满足什么条件"。这一块最容易被新手忽略,也最影响稳定性。模型默认会"努力多帮你做点事",没有约束就会输出多余的解释、铺垫、注意事项。
4. 输出格式(Output Format)——明确说出"返回的形态"。如果下游要程序解析,就必须严格定义 JSON / 标签 / 行格式。
一个把四件套写齐的 Prompt 长这样:
PROMPT = """\
# 角色
你是一位资深的 Python 代码审查员,专长是发现 hooks 误用、性能陷阱和安全问题。
# 任务
审查下面这段 Python 代码,找出潜在的 bug 和改进建议。
# 约束
- 只关注正确性、性能、安全三个维度
- 不要讨论代码风格、命名、注释格式
- 最多输出 5 条建议
- 每条建议必须能在代码中定位到具体行号
# 输出格式
严格按以下 JSON 数组返回,不要任何其他文字:
[
{{"line": 12, "severity": "high", "issue": "...", "fix": "..."}}
]
# 待审查代码
{code}
"""
注意 Python 字符串里 {{ 和 }} 是为了和 .format() 转义搭配使用,下文会讲到。
烂 Prompt 与好 Prompt 的对比
为了让差距更直观,给同一个需求"从一段招聘 JD 里提取关键信息"看两个版本。
烂版本:
帮我看看下面这个 JD 里有什么有用信息。
{jd_text}
好版本:
你是一个招聘信息抽取器。
任务:从 JD 文本中提取以下字段。
输出要求:
- 严格 JSON,不要 markdown 代码块包裹
- 字段缺失时填 null,不要编造
- salary_range 用 "min-max k" 的格式(如 "20-35k")
字段:
{
"title": "职位名",
"company": "公司名",
"location": "城市",
"salary_range": "薪资区间",
"must_have_skills": ["必备技能列表"],
"years_required": "工作年限要求(数字)"
}
JD 文本:
{jd_text}
两者送给同一个模型,烂版本的输出可能是一段散文式的总结,每次结构都不一样;好版本几乎一定是合法 JSON,可以直接 json.loads 解析。差距就在"是否把契约写清楚"。
Few-shot:用例子代替说明
有些任务很难用文字说清楚(比如特定的语气、风格、行业术语映射),但用几个例子一展示就秒懂。这时候用 Few-shot Prompting——在指令后面附上几组 input/output 示例,模型会模仿这个模式作答。
PROMPT = """\
你的任务是把口语化的需求改写成正式的产品需求文档(PRD)条目。
例子:
输入:能不能搞个东西,让用户能一键导出他的所有聊天记录到本地
输出:作为一名用户,我希望能在设置中点击"导出聊天记录"按钮,将我的全部对话历史以 ZIP 格式下载到本地,以便我能离线归档与备份。
输入:希望搜索快一点,现在等半天
输出:作为一名用户,我希望搜索结果能在 200 毫秒内返回,以便我能在不打断思考流的情况下快速找到目标内容。
输入:{user_text}
输出:"""
经验:示例数量通常 2~5 个之间最划算。0 个(zero-shot)够用就别加;超过 5 个的边际效果递减,但 Token 成本线性上升。
思维链(Chain-of-Thought)
对涉及推理、计算、多步分析的任务,加上一句"请一步步思考"或者明确要求"先分析再得出结论",能显著提升正确率。这背后的原理可以理解成:模型在生成最终答案之前,先把自己的推理过程"打草稿"输出出来,相当于借文本本身做了一次外置工作记忆。
PROMPT = """\
判断下面这段代码运行后会输出什么。
请按以下步骤回答:
1. 先逐行分析代码的执行过程
2. 列出关键变量在每一步的值
3. 最后用一行 "答案:xxx" 给出输出结果
代码:
{code}
"""
注意这种写法会让模型输出变长,意味着 Token 消耗增加、响应变慢。如果你只想要最终答案不想看推理过程,可以让模型把推理放在 <thinking> 标签里,再程序剥掉:
import re
text = response_content
final = re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL).strip()
最新的"推理模型"(OpenAI o 系列、DeepSeek R1)已经把这套机制内置进训练,不再需要你显式写"一步步思考",但理解原理对调试普通模型仍然有用。
控制随机性:temperature
LLM 输出的不确定性主要由 temperature 控制,取值范围 0~2,默认通常 1。粗略理解:
0附近——模型每次几乎选概率最高的下一个 token,输出最稳定。适合分类、提取、代码生成等"有标准答案"的任务0.7~1.0——保留一定多样性。适合创意写作、聊天、头脑风暴>1.5——非常随机,容易胡言乱语,几乎没人这么用
抽取信息这类任务别忘了把 temperature 设成 0:
chat(messages, temperature=0)
光靠 temperature=0 并不能让输出 100% 一致(GPU 浮点运算本身有微小不确定性),但可以把变化控制在很小范围内。
输出 JSON:靠纯 Prompt 的几个技巧
很多场景下你需要模型输出可解析的 JSON。在不用结构化输出 API 的前提下,这几招能显著提高合法率:
1. 在 Prompt 末尾就开个头:在指令最后一行直接写 JSON: 或者 {,强制模型从这里开始。
2. 明确禁止 markdown 包裹:模型默认很爱用 ```json ... ``` 把 JSON 包起来。一句"不要使用代码块包裹,直接输出 JSON"通常能压住。
3. 给结构示范而不是描述:与其用文字说"返回一个有 name 和 age 字段的对象",不如直接把目标 JSON 模板贴进去当例子。
4. 限定字段值:能用枚举就用枚举(如 severity 只允许 low|medium|high),别让模型自由发挥。
5. 给一个解析失败的兜底:
import json
raw = chat(messages, temperature=0)
try:
data = json.loads(raw)
except json.JSONDecodeError:
# 常见情况:被 ```json ``` 包裹了
cleaned = re.sub(r"^```(?:json)?|```$", "", raw.strip(), flags=re.MULTILINE)
data = json.loads(cleaned)
这些技巧可以让你解决 80% 的结构化输出问题。剩下那 20% 难啃的边界情况,第 04 篇会用 Pydantic 和 response_format 参数彻底处理。
Prompt 模板化管理
随着项目变大,把 Prompt 塞在 Python 代码里会很难维护。两条建议:
模板字符串用 .format() 或 Jinja2
简单场景用 .format():
PROMPT_TEMPLATE = """\
你是一个 {role}。
任务:{task}
输入:{input}
"""
prompt = PROMPT_TEMPLATE.format(role="翻译器", task="把英文译成中文", input="hello")
复杂模板(条件分支、循环、嵌套)用 Jinja2:
from jinja2 import Template
tmpl = Template("""\
你是一个 {{ role }}。
{% if examples %}
参考示例:
{% for ex in examples %}
- 输入:{{ ex.input }}
输出:{{ ex.output }}
{% endfor %}
{% endif %}
请处理:{{ input }}
""")
prompt = tmpl.render(role="分类器", examples=[...], input="...")
Prompt 抽到独立文件
把每个 Prompt 写成一个 .md 或 .txt 文件放在 prompts/ 目录下,代码里通过 Path(...).read_text() 加载。这样 Prompt 的改动不需要改 Python 代码,也方便做版本对比和 A/B 测试:
from pathlib import Path
PROMPTS_DIR = Path(__file__).parent / "prompts"
def load_prompt(name: str) -> str:
return (PROMPTS_DIR / f"{name}.md").read_text(encoding="utf-8")
prompt = load_prompt("email_classifier").format(content=email_text)别用 Prompt 解决一切
最后一条经验,省你以后很多痛苦:不是所有问题都靠加 Prompt 解决。看到下面这些信号,应该考虑换工程手段而不是继续加规则:
- Prompt 长到接近 2000 字,里面规则互相打架,模型时灵时不灵——拆任务分多步调用
- 输出格式经常坏,加再多"必须返回 JSON"也没用——上结构化输出(第 04 篇)
- 模型不知道某个事实然后开始编——上 RAG(第 05、06 篇)
- 模型需要查实时数据或调用工具——上 Function Calling(第 07 篇)
Prompt 是底层工具,但它有边界。识别边界比把 Prompt 写得更花哨更重要。
下一篇
第 04 篇会进入 结构化输出:靠 Pydantic + response_format 把 LLM 的输出变成可以直接当 Python 对象用的强类型结果。这是 Python 生态做 AI 应用相对其他语言最有优势的一块,也是把 LLM 真正"嵌进系统"的关键一步。
参考资料
- OpenAI Prompt Engineering Guide
- Anthropic Prompt Engineering 课程
- DeepLearning.AI: ChatGPT Prompt Engineering for Developers — Andrew Ng 短课,免费
- Lilian Weng: Prompt Engineering — OpenAI 研究员的系统综述
- Jinja2 文档
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:Prompt 工程:让模型稳定输出你要的结果
本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/03-Prompt工程/
本文最后一次更新为 天前,文章中的某些内容可能已过时!