凌晨三点我在追一个“幽灵”Bug那天晚上线上Agent突然开始胡言乱语。用户问“今天天气怎么样”它回答“根据您的位置建议您带伞因为您昨晚的睡眠质量评分是72分”。这明显是把天气查询和健康建议两个模块的上下文串了。我第一反应是“代码回滚”。但翻Git log发现过去48小时没人动过代码。再查是Prompt里一个标点符号被改了——团队里有人把“请用中文回答”改成了“请用中文回答。”多了一个句号。就是这个句号让模型对指令的权重分配产生了偏移把后一个系统指令的优先级压低了。这就是Agent开发的残酷现实代码没变但行为变了。因为你的“代码”里有一半是自然语言。Prompt 版本控制别把它当配置文件很多团队把Prompt写在代码里或者塞进一个JSON配置文件。这很危险。Prompt不是配置它是可执行的自然语言代码。它和代码一样需要版本管理、diff、回滚、分支。我踩过的坑把Prompt写在代码字符串里# 别这样写这是灾难defget_weather_agent_prompt():return 你是一个天气助手。 请用中文回答。 如果用户问天气请查询API。 问题在哪你没法diff。哪天模型抽风了你都不知道是Prompt变了还是模型变了。更可怕的是不同分支的Prompt可能不一样合并时冲突了你都不知道该保留哪个版本。正确的做法Prompt即代码但独立管理我现在的做法是每个Prompt是一个独立的.py文件用函数返回函数名就是版本号。# prompts/weather_agent_v2_1.pydefget_prompt()-str: 这里踩过坑v2.0版本把请用中文回答放在了最后 结果模型优先执行了前面的指令忽略了语言要求。 v2.1把语言指令提前到第二行解决了。 return 你是一个天气助手。 请用中文回答。 # 注意语言指令必须在前三行内 你的职责是 1. 查询用户所在位置的天气 2. 用简洁的语言描述天气状况 3. 如果用户没有提供位置主动询问 重要规则 - 不要猜测用户位置 - 不要回答非天气相关的问题 - 如果API返回错误告诉用户“暂时无法获取” 然后在主代码里通过一个版本管理器来加载classPromptManager:def__init__(self):self._prompts{}self._load_prompts()defget_prompt(self,agent_name:str,version:str)-str:# 这里踩过坑直接import会污染命名空间# 改用importlib动态加载module_pathfprompts.{agent_name}_v{version.replace(.,_)}try:moduleimportlib.import_module(module_path)returnmodule.get_prompt()exceptModuleNotFoundError:# 回滚到上一个稳定版本logger.warning(fPrompt{version}not found, falling back to stable)returnself._get_stable_prompt(agent_name)这样每个Prompt版本都有独立的文件Git可以追踪每一次修改。回滚时只需要改版本号不需要动代码。模型切换不是换个API Key那么简单很多人觉得模型切换就是改个modelgpt-4变成modelclaude-3。天真了。不同模型的“性格”差异比你想的大得多。同一个Prompt不同模型的表现天差地别我做过一个实验同一个Agent同一个Prompt分别跑GPT-4和Claude-3。GPT-4严格按照指令执行但偶尔会“过度推理”把简单问题复杂化Claude-3更倾向于“理解意图”但有时会忽略细节指令这意味着什么切换模型时Prompt必须跟着调。你不能指望一个Prompt在所有模型上表现一致。我的模型适配层设计classModelAdapter: 每个模型一个适配器负责 1. 将通用Prompt转换为该模型偏好的格式 2. 处理模型特有的参数如temperature范围不同 3. 解析模型返回的格式差异 def__init__(self,model_name:str):self.model_namemodel_name self._load_config()defadapt_prompt(self,base_prompt:str,context:dict)-str:ifgptinself.model_name:# GPT系列喜欢明确的角色设定和结构化指令returnself._gpt_style(base_prompt,context)elifclaudeinself.model_name:# Claude系列喜欢对话式的引导讨厌重复指令returnself._claude_style(base_prompt,context)elifqweninself.model_name:# 国产模型对中文指令更敏感但需要更明确的格式returnself._qwen_style(base_prompt,context)def_gpt_style(self,prompt:str,context:dict)-str:# 这里踩过坑GPT对System Message和User Message的权重不同# 关键指令必须放在System Message里system_msgf你是一个{context.get(role,助手)}。\n{prompt}return[{role:system,content:system_msg},{role:user,content:context.get(user_input,)}]def_claude_style(self,prompt:str,context:dict)-str:# Claude对Human/Assistant格式更敏感# 别这样写把指令放在Human消息里Claude会忽略returnfHuman:{prompt}\n\n{context.get(user_input,)}\n\nAssistant:这样切换模型时只需要改ModelAdapter的实例化参数Prompt会自动适配。但注意适配层不能解决所有问题核心逻辑还是要针对目标模型单独调优。A/B 测试别信直觉信数据做Agent优化最怕什么“我觉得这样更好”。你的直觉大概率是错的。我做过一个测试把Prompt里的“请”字去掉我觉得更简洁结果用户满意度下降了12%。因为模型觉得“不礼貌”回答也变得生硬。我的A/B测试框架classAgentABTest: 轻量级A/B测试不需要第三方服务 注意这里踩过坑不要用随机数做分流要基于用户ID哈希 def__init__(self,experiment_name:str,variants:list):self.experiment_nameexperiment_name self.variantsvariants# [{name: A, prompt_version: 2.1}, ...]self._init_metrics()defget_variant(self,user_id:str)-dict:# 基于用户ID的稳定分流同一个用户始终看到同一个版本hash_valhash(f{self.experiment_name}_{user_id})%100idxhash_val//(100//len(self.variants))returnself.variants[idx]defrecord_metric(self,user_id:str,variant_name:str,metric:str,value:float):# 记录关键指标响应时间、用户满意度、任务完成率、错误率self._metrics[variant_name][metric].append(value)defanalyze(self)-dict:# 简单的统计分析别搞太复杂results{}forvariantinself.variants:namevariant[name]results[name]{avg_response_time:np.mean(self._metrics[name].get(response_time,[0])),completion_rate:np.mean(self._metrics[name].get(completion,[0])),error_rate:np.mean(self._metrics[name].get(error,[0])),sample_size:len(self._metrics[name].get(response_time,[]))}returnresults什么该测什么不该测该测的Prompt措辞的微小变化语气词、标点、顺序模型参数temperature、top_p上下文窗口大小工具调用格式不该测的核心业务逻辑这个应该用单元测试安全相关的Prompt风险太高用户隐私相关的指令一个真实的A/B测试案例我们曾经测试过两种Prompt风格版本A指令式你是一个客服助手。请按以下步骤操作 1. 确认用户问题 2. 查询知识库 3. 给出答案版本B角色扮演式你是一个经验丰富的客服专家。用户来找你帮忙请用专业且友好的态度 - 先理解用户的需求 - 再查找相关信息 - 最后给出清晰的解答结果出乎意料版本B的任务完成率高了8%但平均响应时间慢了1.2秒。因为模型在“角色扮演”上花了更多时间。最后我们选择了版本B但加了一个“如果用户连续追问切换到简洁模式”的规则。版本管理的终极方案Prompt Registry经过多次踩坑我最终设计了一个集中式的Prompt注册中心。它不是一个数据库而是一个版本化的文件系统。prompts/ ├── weather_agent/ │ ├── v1.0.py │ ├── v1.1.py │ ├── v2.0.py │ └── stable - v2.0.py # 符号链接指向当前稳定版本 ├── customer_service/ │ ├── v1.0.py │ ├── v1.1.py │ └── experimental/ │ ├── v2.0_ab_test_a.py │ └── v2.0_ab_test_b.py └── registry.json # 记录每个Agent的版本历史、测试结果、回滚记录每次修改Prompt都创建一个新文件而不是修改旧文件。这样你可以随时回滚到任意历史版本而且可以并行维护多个实验版本。registry.json的内容{weather_agent:{current_version:2.0,history:[{version:1.0,date:2025-01-10,author:张三,change:初始版本},{version:1.1,date:2025-02-15,author:李四,change:修复语言指令位置},{version:2.0,date:2025-03-20,author:王五,change:重构为模块化Prompt}],ab_tests:[{test_id:ab_001,variants:[1.1,2.0],winner:2.0,metrics:{completion_rate:5%}}],rollback_count:2}}个人经验性建议永远不要在生产环境直接改Prompt。哪怕只是加一个空格也要走版本管理流程。我见过最离谱的事故是有人在生产环境的热加载配置里改了一个字导致整个Agent集群崩溃。Prompt的测试要比代码测试更严格。代码有类型检查、单元测试、集成测试。Prompt呢至少要有“回归测试”——用一组固定的测试用例每次改Prompt都跑一遍看输出是否稳定。模型切换的成本比你想象的高。不要因为新模型便宜就盲目切换。适配、测试、调优至少需要两周。而且你永远不知道新模型会在哪个边界条件下“抽风”。A/B测试的样本量要足够大。我见过有人测了100个样本就下结论。对于Agent这种高方差系统至少需要1000个样本才能看到统计显著性。而且要关注长尾效应——有些Prompt在90%的情况下表现很好但在10%的边界情况下会出大问题。建立“Prompt Review”机制。就像Code Review一样每次Prompt变更都要有人审核。审核的重点不是语法而是“这个Prompt在极端情况下会怎么表现”。最后记住一句话Agent的版本管理本质上是管理“不确定性”。代码是确定的但自然语言不是。你越早接受这个事实越早建立相应的工程规范你的Agent就越稳定。