Appearance
Prompt 与上下文
Prompt 就是提示词,一段给模型看的任务说明:要处理什么材料,按什么要求处理,输出时有什么限制。
容易乱的是上下文。模型只看得到这次请求里带过去的内容,本地文件、数据库、联系人列表、前一次脚本运行结果,都不会自动出现在它眼前。Python 没读出来、没拼进请求,模型就只能猜。
比如一句很短的要求:
text
整理一下这句话在人和人之间聊天可能还能靠默契补上,但放到 API 里就太空了。整理成摘要、待办、标题、表格、JSON,模型都可能理解成不同方向。脚本最怕这种飘来飘去的结果,因为后面的代码没法稳定接。
一、要求和材料
提示词里最基本的拆法,就是把要求和材料分开。
要求是稳定的,像规则:
text
把输入文字整理成待办清单。
只保留具体动作。
每行一条。
不要解释。材料是每次变化的内容:
text
今晚买菜,写周报,睡前回复邮件。合在一起发给模型,大概就是:
text
把输入文字整理成待办清单。
只保留具体动作。
每行一条。
不要解释。
输入文字:
今晚买菜,写周报,睡前回复邮件。这里有个小细节:要求越像“规则”,材料越像“数据”,脚本越好维护。要求和材料揉在一起时,后面从文件、表单、接口里读内容,就很容易把用户输入和系统要求混成一团。
Responses API 里可以直接用 instructions 和 input 分开写:
python
response = client.responses.create(
model=model,
instructions="把输入文字整理成待办清单。只保留具体动作。每行一条。不要解释。",
input="今晚买菜,写周报,睡前回复邮件。",
max_output_tokens=200,
)instructions 更像固定规则,input 更像这次传进去的材料。这样写不是为了显得高级,只是为了让脚本里“哪些东西不变、哪些东西会变”更清楚。
二、上下文边界
上下文可以理解成模型这次能看到的全部内容。它不等于电脑里的全部资料,也不等于项目目录里的全部文件。
这个点一开始很容易误会。比如项目里有个 article.txt:
text
今天讨论了三个事项。第一,首页按钮颜色要调整。
第二,接口超时需要排查。第三,周五前补充测试用例。脚本如果只发:
text
总结 article.txt模型并不会打开这个文件。它看到的只是 article.txt 这几个字,最多根据文件名猜。正确做法是 Python 先读文件,再把文件内容放进 input。
新增文件:article.txt
text
今天讨论了三个事项。第一,首页按钮颜色要调整。
第二,接口超时需要排查。第三,周五前补充测试用例。修改 scripts/hello_ai.py 里读取材料和调用 API 的部分:
python
from pathlib import Path
text = Path("article.txt").read_text(encoding="utf-8")
response = client.responses.create(
model=model,
instructions="把输入文字整理成三条简短待办。每行一条,不要解释。",
input=text,
max_output_tokens=200,
)这就是上下文边界最朴素的理解:模型只能处理请求里给它的东西。文件名不是文件内容,路径不是资料本身。
三、变量注入
脚本里的提示词经常要带变量。比如同一段文章,有时要整理成待办,有时要整理成摘要;有时要求中文,有时要求英文。
这种变量不要到处用字符串拼接,最好集中放在一个模板里。
新增文件:scripts/prompt_template.py
python
#!/usr/bin/env python3
"""用模板组织提示词变量。"""
import os
import sys
from pathlib import Path
from openai import OpenAI
PROMPTS = {
"todo": "把输入文字整理成待办清单。每行一条,只保留具体动作,不要解释。",
"summary": "把输入文字整理成一段简短摘要,保留关键事实,不要扩写。",
}
def main():
api_key = os.getenv("OPENAI_API_KEY")
model = os.getenv("OPENAI_MODEL")
if not api_key:
print("缺少环境变量: OPENAI_API_KEY", file=sys.stderr)
return 2
if not model:
print("缺少环境变量: OPENAI_MODEL", file=sys.stderr)
return 2
mode = "todo"
instructions = PROMPTS[mode]
text = Path("article.txt").read_text(encoding="utf-8")
client = OpenAI(api_key=api_key, timeout=60.0)
response = client.responses.create(
model=model,
instructions=instructions,
input=text,
max_output_tokens=300,
)
print(response.output_text.strip())
return 0
if __name__ == "__main__":
raise SystemExit(main())运行:
bash
uv run python scripts/prompt_template.pyPROMPTS 这里先用字典就够了。mode = "todo" 时按待办清单整理,改成 mode = "summary" 就按摘要整理。提示词集中放在一个地方,改规则时不用到代码各处找字符串。
这类小脚本里暂时没必要上复杂模板引擎。str.format()、f-string、字典和函数已经能处理大部分情况。等提示词变得很多、还要按业务模块拆文件时,再考虑单独的模板文件。
四、模板文件
提示词很短时,直接写在 Python 里没什么问题。提示词一长,尤其是换行多、规则多,放在 .py 文件里会把主流程挤得很难看。
可以把提示词挪到单独文件。
新增目录:
text
prompts/PowerShell:
powershell
New-Item -ItemType Directory -Force promptsLinux / macOS:
bash
mkdir -p prompts新增文件:prompts/todo.txt
text
把输入文字整理成待办清单。
规则:
- 每行一条
- 只保留具体动作
- 不要解释
- 不要添加原文里没有的事情脚本里读取这个文件:
python
instructions = Path("prompts/todo.txt").read_text(encoding="utf-8")
text = Path("article.txt").read_text(encoding="utf-8")
response = client.responses.create(
model=model,
instructions=instructions,
input=text,
max_output_tokens=300,
)这样做有个很实际的好处:提示词和代码分开了。代码管读文件、调 API、保存结果;提示词文件只管模型应该怎么处理文本。修改提示词时,不容易顺手碰坏 Python 主流程。
五、分隔符
材料里如果包含很多段话,最好给材料加一个清楚的边界。比如:
text
输入文字:
---
今天讨论了三个事项。第一,首页按钮颜色要调整。
第二,接口超时需要排查。第三,周五前补充测试用例。
---分隔符不是魔法,作用很普通:让模型知道哪一段是规则,哪一段是材料。尤其是材料本身也可能出现“请总结”“不要输出”这类文字时,边界不清楚就容易被材料里的句子带偏。
脚本里可以把输入包一层:
python
wrapped_input = f"""请处理下面分隔符里的文字:
---
{text}
---"""然后把 wrapped_input 传给 input:
python
response = client.responses.create(
model=model,
instructions=instructions,
input=wrapped_input,
max_output_tokens=300,
)这个习惯在处理用户提交的文本时很有用。用户材料里写了“忽略前面的规则”这种句子,模型未必每次都会被带偏,但至少分隔符能让请求结构清楚一点。会改文件、发消息、调用外部系统的动作,还是要在 Python 代码里限制。
六、长文本截断
上下文不是无限的。文章太长、聊天记录太多、FAQ 一次塞太多,都可能超过模型能处理的长度,也会把费用拉高。
基础脚本里可以先做一个很笨但能用的截断:只取前面一部分。
python
def limit_text(text, max_chars=4000):
"""按字符粗略截断文本,避免一次请求塞太多内容。"""
if len(text) <= max_chars:
return text
return text[:max_chars] + "\n\n[内容过长,后续部分已截断]"调用前处理一下:
python
text = Path("article.txt").read_text(encoding="utf-8")
text = limit_text(text, max_chars=4000)字符数不等于 token 数,这只是一个粗略保护。中文、英文、标点、代码都会被模型拆成 token,精确计算要用专门的 tokenizer。小脚本里先用字符数挡一下,至少不会把整本长文一次塞进去。
截断也有代价:后半段内容会丢。比如文章结论在最后,直接截前 4000 字就可能漏掉关键内容。资料变多以后,做法会换成按段落切开,再挑相关段落放进请求里。
七、返回示例
写提示词时,给一个输出示例很有用。模型看到示例后,更容易照着格式返回。
比如待办清单可以这样写:
text
把输入文字整理成待办清单。
输出示例:
- 买菜
- 写周报
规则:
- 每行一条
- 只保留具体动作
- 不要解释如果要模型返回一小段摘要,也可以给示例:
text
把输入文字整理成一句摘要。
输出示例:
会议确认了页面调整、接口排查和测试补充三件事。
规则:
- 只输出一句话
- 不要列项目符号
- 不要添加原文没有的信息示例短一点更好,太长会占上下文,也容易让模型照抄示例里的内容。这里的示例只是告诉它“长什么样”,材料本身还是 input 里的那段文字。
八、一个完整脚本
把模板文件、材料文件、截断和分隔符放在一起,脚本长这样。
新增文件:scripts/run_prompt.py
python
#!/usr/bin/env python3
"""读取提示词模板和输入文本,调用模型返回结果。"""
import argparse
import os
import sys
from pathlib import Path
from openai import OpenAI
def parse_args():
parser = argparse.ArgumentParser(description="run prompt template")
parser.add_argument("--prompt", default="prompts/todo.txt", help="提示词文件")
parser.add_argument("--input", default="article.txt", help="输入文本文件")
parser.add_argument("--max-chars", type=int, default=4000, help="输入文本最大字符数")
return parser.parse_args()
def limit_text(text, max_chars):
"""按字符粗略截断,避免一次请求塞太长。"""
if len(text) <= max_chars:
return text
return text[:max_chars] + "\n\n[内容过长,后续部分已截断]"
def main():
args = parse_args()
api_key = os.getenv("OPENAI_API_KEY")
model = os.getenv("OPENAI_MODEL")
if not api_key:
print("缺少环境变量: OPENAI_API_KEY", file=sys.stderr)
return 2
if not model:
print("缺少环境变量: OPENAI_MODEL", file=sys.stderr)
return 2
prompt_path = Path(args.prompt)
input_path = Path(args.input)
if not prompt_path.exists():
print(f"提示词文件不存在: {prompt_path}", file=sys.stderr)
return 2
if not input_path.exists():
print(f"输入文件不存在: {input_path}", file=sys.stderr)
return 2
instructions = prompt_path.read_text(encoding="utf-8")
text = input_path.read_text(encoding="utf-8")
text = limit_text(text, args.max_chars)
wrapped_input = f"""请处理下面分隔符里的文字:
---
{text}
---"""
client = OpenAI(api_key=api_key, timeout=60.0)
response = client.responses.create(
model=model,
instructions=instructions,
input=wrapped_input,
max_output_tokens=500,
)
print(response.output_text.strip())
return 0
if __name__ == "__main__":
raise SystemExit(main())运行:
bash
uv run python scripts/run_prompt.py --prompt prompts/todo.txt --input article.txt这里多了几个参数:--prompt 指向提示词文件,--input 指向材料文件,--max-chars 控制最大输入长度。脚本当前只做一件事:读模板和材料,拼出清楚的请求,再把模型返回打印出来。
如果运行时报:
text
输入文件不存在: article.txt说明脚本当前目录不对,或者文件还没创建。这个错误比直接抛一大串 FileNotFoundError 好读得多。API 脚本里这种本地检查很值钱,因为它能把“文件问题”和“模型问题”提前分开。
九、提示词调试
提示词调试不能只看最后答案,还得看这次到底发了什么。脚本里可以临时打印请求内容,确认模型看到的东西是不是预期那样。
比如在调用 API 前加几行:
python
print("=== instructions ===", file=sys.stderr)
print(instructions, file=sys.stderr)
print("=== input ===", file=sys.stderr)
print(wrapped_input, file=sys.stderr)调通以后再删掉,或者加一个 --debug 参数控制。调试输出要打到 stderr,这样正式结果还在 stdout,后面重定向保存时不容易混。
提示词写得好不好,回头看几个点就知道了:要求有没有说清楚,材料有没有真的放进请求,边界有没有隔开,输出样子有没有示例,输入是不是太长。