Prompt Engineering:让LLM听懂你的话 — 从硬编码到模板化到多段式

先说结论

Prompt 是 Agent 的"操作系统"。

不是夸张 --- Agent 的每一次决策、执行、校验,都靠 Prompt 告诉 LLM "你是谁、做什么、怎么做、不能做什么"。Prompt 写得好,Agent 就聪明;Prompt 写得乱,Agent 就胡说八道。

工程化的第一步:模板化配置 + 变量注入。 把 Prompt 从代码里抽出来,变成可配置、可版本管理的模板文件。

一、System Prompt vs User Prompt:角色分工

LLM 的每次调用都有两个 Prompt 槽位,分工明确:

System Prompt:告诉 LLM "你是谁"

定义角色身份、专业领域、行为边界。LLM 会以此为"世界观"来约束自己的输出。

复制代码
你是一位美妆赛道的干货科普专家,风格亲和口语化。

这一句话做了三件事:

  • 角色:干货科普专家(不是销售、不是评测博主)
  • 领域:美妆赛道(不会聊投资、不会聊健身)
  • 风格:亲和口语化(不会写论文腔)

User Prompt:告诉 LLM "做什么"

具体的任务指令 + 上下文数据。每次调用都不同。

复制代码
标题:夏季防晒的5个误区
选题方向:夏季防晒推荐
风格约束:语气亲和,emoji中频
请创作正文内容。

为什么分两层?

System Prompt User Prompt
角色 定义身份 下达任务
变化频率 同一人设不变 每次调用都变
存储位置 模板文件 代码动态拼接
类比 操作系统 应用程序

关键原则:System Prompt 越稳定,LLM 的行为越一致。User Prompt 越具体,LLM 的输出越精准。

二、Prompt 的模板化:从硬编码到配置文件

反面教材:Prompt 写死在代码里

ini 复制代码
# ❌ 硬编码:改一个词要改代码、发版
result = await llm.generate(
    system_prompt="你是一位美妆赛道的干货科普专家,风格亲和口语化。",
    user_prompt=f"请写一篇关于{topic}的文章",
)

问题

  • 想改"亲和口语化"为"犀利直白" → 改代码 → 提交 → 发版
  • 想加一条禁忌"不要用'众所周知'" → 改代码 → 提交 → 发版
  • 想给不同赛道用不同 Prompt → if/else 堆积 → 代码膨胀

正面教材:模板文件 + 变量注入

项目中的 prompts/body_generation.md

diff 复制代码
你是一位{{ niche }}赛道的{{ persona_type }},风格{{ writing_style }}。
​
# 任务
基于选题和标题,创作一篇{{ content_format }}正文。
​
# 结构要求
1. 开篇:抓住眼球,3秒内留住读者
2. 核心干货:分3-5段,每段一个小标题+干货内容
3. 总结收尾:简洁总结+轻引导互动
​
# 风格约束
- 语气:{{ writing_style }}
- emoji频率:{{ emoji_frequency }}
- 严格遵循禁忌规则
- 不要使用AI生硬书面语,保持自媒体口语化表达

{{ niche }}{{ writing_style }} 是 Jinja2 占位符,运行时由代码注入真实值。

模板化的好处

  • 改 Prompt 不改代码,直接编辑 .md 文件
  • 不同人设自动适配:美妆人设注入 niche=美妆,职场人设注入 niche=职场
  • Prompt 可以版本管理、Code Review、A/B 测试

模板引擎:PromptManager

项目用 Jinja2 做 Prompt 模板渲染,核心代码 llm/prompt.py

python 复制代码
class PromptManager:
    def __init__(self, prompts_dir: str = "prompts"):
        self.env = Environment(
            loader=FileSystemLoader(str(self.prompts_dir)),
            keep_trailing_newline=True,
        )
​
    def get_template(self, name: str) -> str:
        """获取原始模板(不渲染)"""
        template = self.env.get_template(f"{name}.md")
        return template.render()
​
    def render(self, name: str, **kwargs: str) -> str:
        """渲染模板:占位符 → 真实值"""
        template = self.env.get_template(f"{name}.md")
        return template.render(**kwargs)

调用方式

ini 复制代码
# 获取 System Prompt(模板已渲染占位符)
system_prompt = prompt_mgr.get_template("body_generation")
​
# 或者动态渲染
rendered = prompt_mgr.render("body_generation", niche="美妆", writing_style="亲和口语化")

三、Prompt 的多段式:像搭积木一样组装

一个好的 Prompt 不是一段文字,而是多段组装,每段有明确职责:

复制代码
角色设定 + 任务描述 + 约束条件 + 输出格式

项目中的实战:正文生成的完整 Prompt 拼装

content/body.py 中的 generate_text() 方法,展示了 User Prompt 如何多段组装:

swift 复制代码
user_prompt = (
    # ━━ 第1段:上下文信息 ━━
    f"赛道:{persona.niche}\n"
    f"人设类型:{persona.persona_type}\n"
    f"文案风格:{persona.writing_style}\n"
    f"内容形式:{persona.content_format}\n\n"
​
    # ━━ 第2段:任务数据 ━━
    f"标题:{title}\n"
    f"选题方向:{topic.title}\n"
    f"选题分类:{topic.category}\n\n"
​
    # ━━ 第3段:结构约束 ━━
    f"结构要求:\n"
    f"1. 开篇:{opening_rendered}\n"
    f"2. 核心干货:分3-5段,每段一个小标题+干货内容\n"
    f"3. 总结收尾:{closing_rendered}\n"
    f"4. 段落字数:≤{persona.paragraph_max_chars}字\n\n"
​
    # ━━ 第4段:风格约束 ━━
    f"风格约束:\n"
    f"- 语气:{persona.writing_style}\n"
    f"- emoji频率:{persona.emoji_frequency}\n"
    f"- 禁忌:{banned_rules}\n\n"
)
​
# ━━ 第5段:风格画像(动态注入) ━━
style_hint = StyleLearner.build_style_hint(persona)
if style_hint:
    user_prompt += style_hint + "\n\n"
​
# ━━ 第6段:执行指令 ━━
user_prompt += "请创作正文内容。"

6段式 Prompt,每段职责清晰

职责 变化频率 来源
上下文信息 告诉LLM当前环境 每个人设不同 PersonaConfig
任务数据 具体要处理的内容 每篇文章不同 Topic + Title
结构约束 输出的格式要求 相对人设稳定 PersonaConfig
风格约束 输出的风格限制 相对人设稳定 PersonaConfig
风格画像 从修改中学习的偏好 随使用积累 StyleLearner
执行指令 最终的动作指令 固定 硬编码

为什么不能写成一段?

试想如果把上面6段揉成一段:

arduino 复制代码
你是美妆专家,写一篇关于夏季防晒的文章,风格亲和,emoji中频,
不要用"众所周知",你之前3次要求语气活泼,段落别太长,
开篇要抓眼球,结尾要引导互动,请创作正文内容。

LLM 看到这一坨,大概率会遗漏一半约束。分段 + 标题 + 编号 = LLM 能逐条遵循。

四、约束的艺术:禁忌规则怎么写?

Prompt 中最容易被忽略、但最影响质量的部分 --- 禁忌规则

项目中的禁忌规则构建

python 复制代码
def _build_banned_rules(self, persona: PersonaConfig) -> str:
    rules = []
    if persona.banned_words:
        rules.append(f"禁止使用:{', '.join(persona.banned_words)}")
    if persona.banned_topics:
        rules.append(f"禁止涉及:{', '.join(persona.banned_topics)}")
    # 通用禁忌
    rules.append("禁止使用'众所周知'、'不言而喻'等AI书面语")
    rules.append("禁止编造数据或引用不存在的来源")
    return ";".join(rules)

禁忌规则的三层设计

  1. 人设级禁忌banned_words + banned_topics,每个人设不同
  2. 通用禁忌:所有赛道都适用的规则(不编造数据、不用AI书面语)
  3. 风格画像禁忌:从修改记录中学到的偏好("我之前3次要求减少emoji")

踩坑:禁忌太松 vs 太严

  • 太松:LLM 编造数据、用"众所周知"、写论文腔 → 读者一眼看出是AI
  • 太严:禁了太多词,LLM 无话可说 → 输出空洞
  • 解法:禁"AI书面语",不禁"专业术语";禁"编造数据",不禁"引用常识"

五、输出格式约束:让LLM输出结构化数据

Prompt 最后一段通常是输出格式要求,这直接决定下游代码能不能解析。

选题生成:要求JSON输出

prompts/topic_generation.md 的末尾:

bash 复制代码
# 输出格式
JSON对象:
{
  "topics": [
    {
      "title": "选题方向",
      "category": "knowledge/pitfall/comparison/tutorial/experience",
      "content_format": "适配内容形式",
      "estimated_potential": "high/medium/low"
    }
  ]
}

标题生成:要求JSON数组 + 额外字段

prompts/title_generation.md 的末尾:

bash 复制代码
# 输出格式
JSON数组:[{"title": "...", "formula_type": "...", "estimated_ctr": 0.0-1.0}]

为什么必须指定输出格式?

不指定 指定
LLM 可能返回一整段散文 返回结构化JSON
下游代码无法解析 json.loads() 直接用
每次格式不同 格式稳定可预期

但即使指定了JSON,LLM也可能返回 ```json 代码块,所以下游一定要加容错解析:

ini 复制代码
# 容错解析:去掉 markdown 代码块
result = result.strip()
if result.startswith("```"):
    lines = result.split("\n")
    lines = [l for l in lines if not l.startswith("```")]
    result = "\n".join(lines).strip()
data = json.loads(result)

六、风格画像注入:Prompt 的动态进化

这是项目中最有"Agent味"的 Prompt 设计 --- 风格画像不是写死在模板里的,是从修改记录中动态学习、动态注入的

css 复制代码
# content/body.py --- 风格画像注入
from ..persona.style_learner import StyleLearner
style_hint = StyleLearner.build_style_hint(persona)
if style_hint:
    user_prompt += style_hint + "\n\n"

当人设积累了风格画像后,Prompt 会自动多出一段:

diff 复制代码
【风格画像】
倾向于活泼口语化表达;emoji使用克制;喜欢用数据支撑观点;段落简短有力

【风格偏好提醒】
- 语气:活泼(我之前 3 次要求此调整)
- emoji:低频(我之前 2 次要求此调整)

这不是开发者写的 Prompt,是 Agent 自己从用户行为中学到的 Prompt。 这就是 Agent 和普通软件的本质区别 --- Agent 的 Prompt 会进化。

踩坑总结

根因 修复
Prompt 写死在代码里 没有模板化 抽成 .md 模板文件 + Jinja2 渲染
改一个词要改代码发版 Prompt 和逻辑耦合 Prompt 独立文件,代码只负责注入变量
LLM 输出格式不稳定 没有指定输出格式 Prompt 末尾加 JSON schema 约束
LLM 返回 ```json 代码块 LLM 的 markdown 习惯 加容错解析,去掉代码块标记
禁忌太松,AI书面语满天飞 通用禁忌缺失 加"禁止AI书面语"通用规则
风格每次随机 Prompt 没有记忆 风格画像动态注入 Prompt

经验总结

  1. Prompt 是 Agent 的操作系统,System Prompt 定义身份,User Prompt 下达任务,两者分工明确
  2. 模板化 + 变量注入是工程化的第一步,改 Prompt 不改代码,不同人设自动适配
  3. 多段式 Prompt = 角色设定 + 任务描述 + 约束条件 + 输出格式,分段越清晰,LLM 遵循越好

下篇预告

下一篇讲 Pydantic 数据建模 --- 给 Agent 装上结构化思维,让 LLM 的输出不再"胡说八道"。

相关推荐
MomentYY3 小时前
RAG 混合检索:关键词 + 语义
人工智能·agent·ai编程
用户3126874877203 小时前
AI Agent 开发实战(八):输出 Schema 约束与结构化输出
langchain·ai编程
Python私教4 小时前
Codex 写出的代码能跑却算错钱:我用 3 个测试拆穿一次 AI 编程幻觉
python·单元测试·ai编程
Python私教4 小时前
我只写了一个 add 工具,终于把 MCP 的 Host、Client、Server 跑明白了
python·ai编程·mcp
名不经传的养虾人7 小时前
从0到1:企业级AI项目迭代日记 Vol.77|隔离不只是数据,还有进程、上下文和依赖
大数据·人工智能·ai编程·企业ai·多agent协作
9i编程8 小时前
AI BI Helper 开发实录 02:Graph 工作流编排——SQL 生成、执行与邮件推送
人工智能·openai·ai编程
leeyi8 小时前
切片策略选错了,检索效果天差地别(第68篇-E54)
aigc·agent·ai编程
1名持续学习的码农8 小时前
Codex 任务总中断:ChatGPT Plus 用户该升级 Pro,还是先把需求写清?
人工智能·gpt·ai·ai编程·codex
怕浪猫9 小时前
第11章 实战项目二:自动化研发运维Agent
aigc·agent·ai编程