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

📅 2026/7/31 13:23:07 👁️ 阅读次数 📝 编程学习
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 PromptUser Prompt
角色定义身份下达任务
变化频率同一人设不变每次调用都变
存储位置模板文件代码动态拼接
类比操作系统应用程序

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

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

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

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

问题

  • 想改"亲和口语化"为"犀利直白" → 改代码 → 提交 → 发版

  • 想加一条禁忌"不要用'众所周知'" → 改代码 → 提交 → 发版

  • 想给不同赛道用不同 Prompt → if/else 堆积 → 代码膨胀

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

项目中的prompts/body_generation.md

你是一位{{ 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

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)

调用方式

# 获取 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 如何多段组装:

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段揉成一段:

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

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

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

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

项目中的禁忌规则构建

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的末尾:

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

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

prompts/title_generation.md的末尾:

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

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

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

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

# 容错解析:去掉 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 设计 —风格画像不是写死在模板里的,是从修改记录中动态学习、动态注入的

# 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 会自动多出一段:

【风格画像】 倾向于活泼口语化表达;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 的输出不再"胡说八道"。