AIGC 提示词优化、AI 生成内容优化这两类岗位需求量正在快速增长,但很多团队卡在同一个问题上:到底该为“更好的 prompt”投入多少 API 预算?这次我们来看 NPO 单线提示优化与 GEPA 全局并行优化这两种路线。NPO 走单线串行迭代,GEPA 走多候选并行搜索,前者以更少预算追平后者的效果是完全可行的,前提是流程设计得足够克制。这篇文章不讨论理论空转,直接给出一套可在本地落地执行的提示词优化方案:两种优化路线的差异、单线优化流程、可复用 Python 脚本、评估口径、成本控制方法和排查清单。
如果你的目标只是把某一个文本生成任务调稳,比如摘要生成、JSON 抽取、客服话术改写,且 API 预算有限,NPO 会是最合理的选择。GEPA 的搜索空间更大,但每一轮都要同时评估多条候选 prompt,成本和耗时成倍上涨。文章里的脚本不绑定具体厂商,兼容 OpenAI、Claude、通义等 Chat Completion 风格接口,只要把base_url、model、api_key换成自己的服务即可。判断“追平”的标准也不靠感觉,而是基于同一个评估集上的得分差、胜率和成本差。
1. 核心能力速览
先给一张总览表,把 NPO 和 GEPA 的通用特征放在一起对比。下面的内容不是某个具体项目的实测数据,而是从提示优化工程中提炼出的通用判断,实际数值需要按你自己的模型和评估集测试。
| 对比维度 | NPO 单线提示优化 | GEPA 全局并行提示优化 |
|---|---|---|
| 优化路径 | 从一条基线 prompt 出发,串行修改 | 同时初始化多条 prompt,交叉、变异、批量评估 |
| 预算开销 | 低,每轮只评估少量候选 | 高,需要大量并行评估资源 |
| 收敛速度 | 单任务方向明确时收敛快 | 探索空间大,前期波动更明显 |
| 适用场景 | 预算敏感、单一任务、要求稳定输出 | 任务类型多、需要大规模搜索、能接受高成本 |
| 实现复杂度 | 相对简单,脚本即可控制 | 需要调度队列、并发评估和结果汇总 |
| 主要风险 | 容易陷入局部最优 | 容易在无效候选上浪费预算 |
表格中提到的 NPO 和 GEPA,在不同文章里可能用不同英文全称。这里不纠结缩写命名,统一理解为两种主线:NPO 是单线串行优化,GEPA 是全局并行优化。后续所有流程都围绕“如何用 NPO 的方式逼近 GEPA 的效果”展开。
2. 适用场景与使用边界
NPO 单线优化适合哪些团队?第一类是个人开发者和独立产品端,没有太多 token 预算,但希望把某个核心 prompt 调到稳定可用的水平。第二类是小团队想要快速验证“提示词优化这件事到底有没有用”,先用单条 prompt 跑通闭环,再决定要不要上更重的并行搜索。第三类是把 prompt 作为内部工具的企业团队,比如客服摘要、审批意见抽取、内容分类,这类任务通常要求稳定输出,不需要非常发散的风格探索。
NPO 不适合什么场景?如果你的任务本身边界很模糊,用户输入差异巨大,单一 prompt 很难覆盖所有情况,那么单线优化很容易过拟合到评估集。另一个不适合的场景是评估成本极高的情况。这里说的成本不只是 API 费用,还包括人工打分时间。如果你每次评估都要三个人逐条看结果,NPO 的“每轮小步迭代”同样会拖慢进度,因为每一轮都可能需要大量人工参与。GEPA 虽然并行评估开销大,但对“哪一个 prompt 方向更可能有潜力”的判断更激进,有时反而能更快跳出局部最优。
使用边界必须明确:涉及真实用户数据、隐私数据、版权数据时,不能直接放进评估集;生成内容如果用于公开渠道或商业场景,需要做人工复核;无论使用哪家 API,都要遵守平台服务条款,不用于绕过内容审核、生成违规信息或进行未经授权的自动化操作。提示词本身也是一种可被复用的资产,如果它包含内部业务规则,注意脱敏和权限管理。
3. 环境准备与前置条件
NPO 单线优化不依赖特殊硬件,普通开发机就能跑。真正的前置条件是:能稳定调用大模型接口、有一个可重复的评估集、有一套记录每次迭代结果的日志机制。下面的清单适合大多数提示词优化项目。
- 操作系统:Windows / macOS / Linux 均可。
- Python:建议 3.9 或以上。
- 依赖库:
openai或requests、pandas、tenacity、pyyaml。 - LLM API:任意 Chat Completion 兼容接口,需要
api_key、base_url、model。 - 评估数据:至少准备一组输入样例和参考答案,样例要覆盖典型场景和边界情况。
- 日志目录:用于保存每轮 prompt、得分、token 消耗和耗时。
建议先建立虚拟环境,避免依赖冲突。
python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install openai pandas tenacity pyyaml requests接口信息用环境变量管理,不要把密钥写死在代码里。
LLM_API_KEY=your_key_here LLM_BASE_URL=https://api.openai.com/v1 LLM_MODEL=gpt-4o-mini如果是本地部署的模型服务,比如 vLLM、Ollama 或者兼容接口的网关,把LLM_BASE_URL改成实际地址即可。模型名也要改成服务端实际加载的模型名。这里的关键是让“调用模型”这个动作变成统一的函数,后续优化流程不关心底层模型是谁。
4. 单线提示优化核心流程
NPO 单线优化的核心原则是“一条基线 prompt,一次只改一个维度”。它和 GEPA 最大的不同在于:GEPA 会同时生成几十条候选 prompt,NPO 只保留一条当前最优,然后围绕它生成下一个候选。这样做的好处是每次效果变化都能归因到某一次修改,坏处是有可能陷入局部最优。下面给出适合大多数任务的标准流程。
4.1 固定任务描述
先把任务边界写清楚。比如“从客服对话中提取用户诉求,输出 JSON 格式,包含诉求类型和优先级”。任务描述不要写成 prompt,它是给评估者和优化脚本看的说明。任务越聚焦,NPO 的效果越容易追上 GEPA。如果一个任务能拆成多个独立子任务,拆开比硬撑一条 prompt 更稳妥。
4.2 构建小型但有效的评估集
评估集不必一开始就很大,但必须保证三个特点:有正确答案或参考输出、包含典型样本、包含边界样本。以摘要任务为例,评估集可以准备 20 到 50 条新闻文本和对应的参考摘要。典型样本占 70%,边界样本占 30%。这部分是整个优化流程的地基,地基不稳,后面所有分数都没有意义。
4.3 评估基线 prompt
先用一个初始 prompt 跑完全部评估集,记录平均分。这一步不需要优化,只要确认评估流程能跑通。如果基线得分接近满分,说明任务太简单,先换个更难的任务。如果得分非常低,先检查是 prompt 问题还是评估口径问题。基线分数是后续所有决策的参照点。
4.4 单线修改策略
每一轮只修改一个变量。常见变量包括:角色设定、输出格式、约束条件、示例数量、语气风格、处理步骤。比如第一轮只加“你是资深数据分析师”,第二轮只改“输出必须是 JSON”,第三轮再加一个 few-shot 示例。不要同时改两个以上变量,否则出问题时无法定位。生成候选 prompt 的方式可以手动,也可以让 LLM 根据上一轮结果生成一个相邻版本。
单线修改到一定阶段后会出现得分平台期。这时记录本轮 prompt 和得分,再尝试换一个修改维度。NPO 的收敛路径不是直线,但每步方向清晰,最终找到的 prompt 通常比盲目并行搜索得到的更可控。
4.5 回归验证与冻结
优化到预算上限后,把最后获得的 prompt 放到一个保留测试集上跑一次回归。这个保留集在优化过程中从未参与过评分,目的是检查是否过拟合。如果保留集得分明显低于评估集得分,说明优化过头了,应该回退到之前某一轮效果更好的版本。确认没问题后,把该 prompt 保存为正式版本,并记录对应的评估结果。
5. 接口 API 调用与批量评估脚本
这一部分给出可直接修改使用的 Python 脚本。脚本思路很简单:封装模型调用函数,定义评估函数,执行单线优化循环。代码不依赖特定框架,适合作为原型改造。
5.1 模型调用封装
用requests直接请求 Chat Completion 接口,减少大型 SDK 的依赖。实际使用时,把环境变量配置好即可。
import os import time import requests API_KEY = os.getenv("LLM_API_KEY") BASE_URL = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1") MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini") def call_llm(system_prompt: str, user_text: str, temperature: float = 0.2) -> str: headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } payload = { "model": MODEL, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_text}, ], "temperature": temperature, } resp = requests.post( f"{BASE_URL}/chat/completions", headers=headers, json=payload, timeout=60, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]如果使用的模型服务要求不同的请求格式,比如增加了chat_template_kwargs或自定义参数,需要按实际接口调整。可以在一个独立文件中维护这个函数,后续优化脚本都调用它。
5.2 评估函数示例
评估函数是整个优化闭环的“裁判”。规则简单时可以用字符串匹配或字段抽取判断,比如 JSON 是否合法、是否包含某关键词;复杂时可以让另一个 LLM 打分,或者人工抽检。下面是同时支持自动规则和自定义打分函数的示例。
from typing import Callable, Dict, List JudgeFn = Callable[[str, str], float] def judge_jsonable(generated: str, reference: str) -> float: import json try: json.loads(generated) return 1.0 if generated.strip() else 0.5 except Exception: return 0.0 def evaluate_prompt( system_prompt: str, eval_inputs: List[Dict[str, str]], judge_fn: JudgeFn, ) -> float: total = 0.0 for item in eval_inputs: generated = call_llm(system_prompt, item["input"]) total += judge_fn(generated, item["reference"]) return total / len(eval_inputs) if eval_inputs else 0.0注意,judge_fn返回 0 到 1 之间的得分。如果想让评估更稳定,可以连续运行两次取平均,避免模型随机性干扰。但这样会增加预算,所以要在稳定性和成本之间取平衡。
5.3 单线优化循环
优化循环负责“基于当前最优 prompt,生成下一个候选,评估并决定是否替换”。这里用一个candidates_fn抽象候选生成策略,它可以是手动输入的 prompt,也可以是一个调用大模型生成相邻版本的函数。
def single_line_optimize( baseline_prompt: str, eval_inputs: List[Dict[str, str]], judge_fn: JudgeFn, candidates_fn, budget: int, history: List[str], ): best_prompt = baseline_prompt best_score = evaluate_prompt(best_prompt, eval_inputs, judge_fn) for step in range(budget): next_prompt = candidates_fn(best_prompt, history) if next_prompt in history: continue score = evaluate_prompt(next_prompt, eval_inputs, judge_fn) history.append(next_prompt) log_entry = { "step": step, "score": score, "best_score": best_score, "prompt": next_prompt, } print(log_entry) if score > best_score + 1e-6: best_prompt = next_prompt best_score = score return best_prompt, best_scorebudget是总优化轮数。建议从 20 到 50 开始,不要一上来跑几百轮。history列表用来避免重复评估相同 prompt。实际工程中,history还要落盘,方便中断后恢复。
5.4 curl 验证最终 prompt
优化结束后,可以用一条简单请求验证最终 prompt 是否可用。
curl -X POST "${LLM_BASE_URL}/chat/completions" \ -H "Authorization: Bearer ${LLM_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "model": "'"${LLM_MODEL}"'", "messages": [ {"role": "system", "content": "在这里粘贴优化后的最佳 prompt"}, {"role": "user", "content": "在这里粘贴测试输入"} ], "temperature": 0.2 }'6. 评估指标与“追平 GEPA”的判定方法
NPO 和 GEPA 的对比不能只看最终 prompt 的生成效果,必须把评估口径统一。最实用的做法是:让 NPO 最优 prompt 与 GEPA 最优 prompt 在同一个保留评估集上做盲测,计算以下三项。
平均得分:把每个样本的评分累加后取平均,直接反映整体水平。胜率:统计 NPO prompt 的生成结果在“成对比较”中优于 GEPA prompt 的样本比例。稳定性:多次运行同一批样本,观察得分的标准差,标准差越小越稳定。
下面给出一个结果记录示例,实际数字需要你自己跑。
{ "task": "summarize", "method": "NPO", "eval_size": 30, "budget": 50, "baseline_score": 0.72, "best_score": 0.88, "gepa_score": 0.89, "win_rate": 0.53, "total_cost_usd": 0.52, "best_prompt": "......" }“追平”的判定可以设一个阈值,比如 NPO 与 GEPA 的平均得分差不超过 0.02,同时胜率不低于 0.45,就可以认为追平。如果 GEPA 的得分明显更高,优先检查是不是 NPO 的候选生成策略太保守,导致每次只在同一个维度上微调。另一种可能是指标本身不稳定,需要先增大评估集或降低温度。
7. 预算控制与性能观察
预算控制是 NPO 相对 GEPA 最明显的优势。但如果不加控制,单线优化也可能偷偷烧掉大量 token。常见失控原因是:候选 prompt 生成长度过高、评估样本数过多、相同候选反复评估、无缓存。下面给出一套可落地的预算控制方法。
在开始优化前,先定义总预算。比如“最多 50 次模型调用,每次输出不超过 300 token”。然后在代码里加入统一的预算计数,每次调用前后记录 token 使用量,使用量达到上限就提前终止。更稳的做法是使用tenacity做重试,并加缓存。
pip install diskcache tenacity可以实现一个简单缓存装饰器:相同输入参数直接读缓存,不重复调用模型。由于 prompt 优化时同一候选可能被重复评估,缓存的收益非常大。
from diskcache import Cache cache = Cache("./.cache") def cached_call_llm(system_prompt, user_text, temperature=0.2): key = (system_prompt, user_text, temperature) if key in cache: return cache[key] result = call_llm(system_prompt, user_text, temperature) cache[key] = result return result性能观察要注意三个维度:API 延迟、并发收益、成本分布。API 延迟主要受模型服务和请求长度影响,串行执行时优化时间会被拉长。可以在评估集上做小并发,一次并发处理 3 到 5 个样本,减少等待时间。并发过高容易触发限流,需要加退避重试。
成本分布最容易出现意外的部分是候选 prompt 生成。让 LLM 生成下一版 prompt 时,最好限制max_tokens,例如只允许输出 500 token。评估阶段的输出长度也建议固定。可以把每轮的 prompt、得分、输入输出 token、耗时写入日志,方便后续复盘。
8. 常见问题与排查方法
提示词优化是一个迭代过程,踩坑很常见。下表整理了使用 NPO 单线优化时最容易遇到的问题和排查思路。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 得分越优化越低 | 单次修改过大,评估集噪声大 | 查看每一轮 prompt 的 diff 和得分 | 一次只改一个变量,增加评估样本 |
| 成本快速超出预算 | 重复评估相同候选、输出 token 过多 | 检查缓存命中率和 token 日志 | 引入缓存,限制 max_tokens |
| 结果不稳定 | temperature 过高 | 多次运行同一输入比较输出 | 固定 temperature 为 0.2 或更低 |
| 接口调用报错 | API Key 或 base_url 配置错误 | 用 curl 单条请求测试 | 检查环境变量和模型名 |
| 过拟合评估集 | 评估样本较少且长期固定 | 在保留集上做回归 | 扩充边界样本,定期更新评估集 |
| NPO 始终追不上 GEPA | 候选生成策略太保守或搜索空间太窄 | 检查每轮修改维度是否重复 | 引入更大胆的 prompt 生成策略,或小幅并行 |
| 人工评估耗时过长 | 评估样本量太大 | 拆分评估集,先看典型样本 | 用 LLM judge 初筛,人工只抽检 |
排查原则是“先看日志,再改代码”。如果某一轮得分异常,先确认是不是评估函数本身有问题。比如 JSON 解析失败被记成 0 分,但模型输出其实包含可用 JSON,只是前后有杂散文本。这种情况下应该先调整解析函数,而不是继续优化 prompt。
9. 最佳实践与工程化建议
NPO 单线优化想做得稳定,需要把“优化过程”当软件工程来管理,而不是靠感觉手动调 prompt。下面几条建议可以直接用起来。
第一,prompt 版本管理。每个版本的 prompt 都要有名称、修改时间、得分、变更说明。建议把最优 prompt 存在单独的prompts/目录,同时把每次评估结果保存为 JSONL 文件。这样回退到历史版本时,不需要重新跑评估。
第二,评估集要定期更新。长期不变的评估集会让优化过拟合到固定样本。每次上线新 prompt 前,加入一批新的真实输入,重新跑一次回归。如果新增样本的得分明显低于原有评估集,说明 prompt 泛化能力不足。
第三,先小参数跑通,再扩大。第一次运行优化时,budget 设为 10,评估集设为 10 条样本。确认全流程没有报错,再逐步扩大。不要一上来就跑到 500 轮,成本很难控制。
第四,接口服务要限制访问范围。如果优化脚本部署在服务器上,确保 API key 不暴露给前端或外部服务。对于敏感数据,可以在本地调用私有化部署模型,或使用数据脱敏后的代理数据集。
第五,涉及人脸、声音、版权素材等生成内容时,必须确认授权。提示词优化虽然面向文本生成,但评估集如果包含他人创作的内容、个人隐私信息或受版权保护的文本,需要先获得合法授权。生成的文本如果用于公开传播,也要人工复核,避免出现事实错误或侵权风险。
10. 总结与下一步
NPO 单线提示优化最能体现价值的地方是:预算有限时,用一条基线 prompt、一组稳定的评估样本、一个可复现的迭代脚本,就能把生成效果逐步拉高。它不一定在所有任务上都战胜 GEPA,但在大多数“单一文本生成任务”上,以更少预算追平 GEPA 是完全可以做到的。最值得先验证的功能是 5.3 节里的优化循环,把 baseline prompt、评估集和 judge_fn 换成自己的任务,跑 20 轮看趋势。
最容易踩的坑有两个:一是评估集质量太差,得分无法反映真实效果;二是单次修改过大,导致效果变化无法归因。建议第一次做时,修改维度最小化,每一轮只加一个角色设定、一个格式约束或一个示例。等流程稳定后,再尝试让 LLM 自动生成候选 prompt,把 NPO 从手动调优升级成半自动优化。
后续可以继续扩展的方向包括:多任务并行单线优化、把 NPO 最优结果作为 GEPA 的种子候选、引入更复杂的 LLM judge 替代简单规则、把优化流程封装成 CLI 工具直接集成到 CI。每扩展一步,预算控制和日志记录都要同步跟上。先把单线闭环跑稳,再谈更大规模的优化。