先说结论
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)
禁忌规则的三层设计:
- 人设级禁忌 :
banned_words+banned_topics,每个人设不同 - 通用禁忌:所有赛道都适用的规则(不编造数据、不用AI书面语)
- 风格画像禁忌:从修改记录中学到的偏好("我之前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 |
经验总结
- Prompt 是 Agent 的操作系统,System Prompt 定义身份,User Prompt 下达任务,两者分工明确
- 模板化 + 变量注入是工程化的第一步,改 Prompt 不改代码,不同人设自动适配
- 多段式 Prompt = 角色设定 + 任务描述 + 约束条件 + 输出格式,分段越清晰,LLM 遵循越好
下篇预告
下一篇讲 Pydantic 数据建模 --- 给 Agent 装上结构化思维,让 LLM 的输出不再"胡说八道"。