|
| 1 | ++++ |
| 2 | +date = '2025-08-28T8:00:00+08:00' |
| 3 | +draft = false |
| 4 | +title = 'Prompt Origanization' |
| 5 | +tags = ['Prompt', 'LLMs'] |
| 6 | ++++ |
| 7 | + |
| 8 | +这篇文章旨在介绍 Python 中常用的提示词组织方式 |
| 9 | + |
| 10 | +### f-string |
| 11 | +使用 f 字符串填充变量得到提示词 |
| 12 | +```Python |
| 13 | +def get_prompt(query: str) -> list[dict]: |
| 14 | + SYSTEM_PROMPT = f"""... |
| 15 | +... |
| 16 | +多行提示词, 也可以填充变量 |
| 17 | +""" |
| 18 | + USER_PROMPT = f"""INPUT: |
| 19 | +{query} |
| 20 | +.... |
| 21 | +""" |
| 22 | + return [ |
| 23 | + {"role": "system", "content": SYSTEM_PROMPT}, |
| 24 | + {"role": "user", "content": USER_PROMPT}, |
| 25 | + ] |
| 26 | +``` |
| 27 | +这种方法实现简单, 速度快, 但是: |
| 28 | +1. 多行字符串由于填充变量的需要, 需写在函数内, 导致代码格式混乱 |
| 29 | +2. 通过代码构造提示词, 任何修改都需要修改代码, 扩展性差 |
| 30 | + |
| 31 | + |
| 32 | +### string.Template |
| 33 | +使用 Python 元素字符串模板 |
| 34 | +```Python |
| 35 | +SYSTEM_PROMPT = string.Template("""你是一名$role |
| 36 | +多行提示词... |
| 37 | +""") |
| 38 | + |
| 39 | +USER_PROMPT = string.Template("""INPUT: |
| 40 | +$query |
| 41 | +""") |
| 42 | + |
| 43 | +def get_prompt(role: str, query: str) -> list[dict]: |
| 44 | + system_prompt = SYSTEM_PROMPT.subtitute(role="助手") |
| 45 | + user_prompt = USER_PROMPT.subtitute(query="问题...") |
| 46 | + return [ |
| 47 | + {"role": "system", "content": system_prompt}, |
| 48 | + {"role": "user", "content": user_prompt}, |
| 49 | + ] |
| 50 | +``` |
| 51 | +使用模板字符串, 模板则不必写在函数内, 且模板字符串可以选择替换部分变量, 使用 `.safe_substitute()`方法传入一个字典, 例如 `{"query": "问题..."}`, 对没有传入的变量解析为 `$var` |
| 52 | +对比 f-string, 模板字符串更加灵活, 且可以只传入部分值 |
| 53 | + |
| 54 | + |
| 55 | +### Jinja2 |
| 56 | +Jinja2 是一个现代的设计者友好的, 仿照 Django 模板的 Python 模板语言. 它速度快, 被广泛使用, 并且提供了可选的沙箱模板执行环境保证安全: |
| 57 | +例如下面这个 `.j2` 文件内容, 构造了一个用于少样本提示的模板 |
| 58 | +```Jinja |
| 59 | +{% if examples %} |
| 60 | +{% for example in examples %} |
| 61 | +INPUT: |
| 62 | +{{ example.input }} |
| 63 | +
|
| 64 | +OUPUT: |
| 65 | +{{ example.output }} |
| 66 | +
|
| 67 | +{% endfor %} |
| 68 | +{% endif %} |
| 69 | +INPUT: |
| 70 | +{{ user_input }} |
| 71 | +``` |
| 72 | +导入该模板文件代码如下: |
| 73 | +```Python |
| 74 | +from jinja2 import Environment, PackageLoader # 根据需要不同也可以使用 FileSystemLoader |
| 75 | + |
| 76 | +env = Environment( |
| 77 | + loader=PackageLoader("app.module.prompt", "template"), |
| 78 | + trim_blocks=True, # 移除 {% ... %} 块前后的多余空白 |
| 79 | + lstrip_blocks=True, # 移除行首 {% ... %} 块前的空白 |
| 80 | +) |
| 81 | + |
| 82 | +def get_prompt(user_input: str) -> list[dict]: |
| 83 | + system_template = env.get_template("system_template.j2") |
| 84 | + user_template = env.get_template("user_template.j2") |
| 85 | + |
| 86 | + system_data = {"var": val, ...} |
| 87 | + user_data = { |
| 88 | + "examples": [ |
| 89 | + {"input": "示例输入1", "output": "示例输出1"}, # 具体样例也可以通过函数传入 |
| 90 | + {"input": "示例输入2", "output": "示例输出2"}, |
| 91 | + ], |
| 92 | + "user_input": user_input", |
| 93 | + } |
| 94 | + |
| 95 | + messages = [ |
| 96 | + {"role": "system", "content": system_template.render(system_data)}, |
| 97 | + {"role": "user", "content": user_template.render(user_prompt)}, |
| 98 | + ] |
| 99 | + |
| 100 | + return messages |
| 101 | +``` |
| 102 | +使用 Jinja2 模板文件的好处是: |
| 103 | +1. 方便组织提示词文件, 例如这里是将提示词文件放在 `ProjectRoot/app/module/prompt` 里面, 模板文件放在 `prompt/template` 里面, 在提示词文件中导入模板文件十分方便, 文件组织清晰, 代码可读性高, 且方便扩展 |
| 104 | +2. 提示词灵活性更好, 对比 string.Template, Jinja2 模板不仅可以填充变量, 还可以在模板中插入循环和条件判断等语法, 使得代码中只需提供一个字典格式的数据即可, 无需在代码里拼凑提示词, 也方便和 RAG 系统结合使用 |
| 105 | + |
| 106 | +虽然 Jinja2 对比 string.Template 性能上要差一些, 但是 LLM 应用真正花时间的地方是模型的推理部分, 相比之下提示词渲染的时间几乎可以忽略不计. 如果提示词非常多, Jinja2 还提供了异步渲染功能, 可以结合异步框架进一步提升性能. |
| 107 | + |
| 108 | +### Wrapping Up |
| 109 | +上面就是近期使用的一些构造提示词的方法, 分别是 `f-string`、`string.Template` 和 `Jinja2`. |
| 110 | +当然也有像 `langchain_core.prompts.prompt.PromptTemplate` 这样专用框架提供的提示词模板功能, 但是为了支持 LangChain LCEL 语法等原因, 导致其类型设计十分抽象, 且 LangChain 对新模型和新功能的支持比较缓慢, 加上版本不稳定, 接口经常变动, 故没有考虑使用 LangChain 框架提供的功能.(实际上, langchain 也支持使用 Jinja2 模板) |
| 111 | +总之, 上面介绍的提示词构造方法各有优劣, 应该根据你项目的复杂度, 自行选择合适的提示词构造方式. |
0 commit comments