一次 Prompt 改动,让客服机器人开始「编故事」
去年某电商平台升级客服助手的 system prompt,把原来的「请严格基于订单数据回答」改成了「请友好地帮助用户解决问题」。改动当天下午,用户开始反馈:AI 告诉他们的退款时效比实际政策快了两天。
问题不难找到------但修复花了 47 分钟。这 47 分钟里,Prompt 变更对每一个打进来的用户全量生效,没有任何办法收窄影响范围,只能等工程师手动回滚部署。
这是大多数 LLM 应用团队都经历过的场景:Prompt 改动不像代码改动那样走 PR review + 分支部署流程,但它的影响范围和生产风险完全不亚于一次服务发布。
Stanford HAI 的 AI Index 2026 报告指出,2025 年文档化 AI 事故同比增加了 55%,而与此同时,把自己的 AI 事故响应评为"优秀"的组织从 28% 降到了 18%。问题不在于出事的频率,而在于出了事之后的控制能力。
Feature Flag 是软件工程里处理"部署但不发布"(deploy but not release)问题的标准答案。把它引入 LLM 应用的控制面,不是什么新奇方案,但绝大多数团队都做得非常粗糙------或者根本没做。
这篇文章讲的是怎么系统性地把 Feature Flag 用到 LLM 工程里,包括 flag 类型的设计、代码层的实现模式,以及 5 个很容易踩到的生产陷阱。
为什么 LLM 应用需要 Feature Flag
传统 Feature Flag 解决的是「代码上线了但功能不想立刻暴露给所有用户」的问题。在 LLM 应用里,这个需求的来源更多、更难预测。
难点一:Prompt 变更是隐性发布
Prompt 不在代码仓库里管理(或者管理得很松散)。改一个 system prompt,不会触发 CI/CD 流水线,不会留下 git commit,不会经过代码审查。但它的效果相当于一次全量的功能发布------所有用户的下一次请求都会走新 Prompt。
Pete Hodgson 在 OpenFeature 治理委员会的定义里说:"Feature flag 让我们可以把'潜伏代码'(latent code)发到生产------代码已经部署,但功能被禁用的 flag 挡在后面。"Prompt 的灰度需要一样的机制。
难点二:模型版本升级是高风险变更
你不可能通过 staging 环境完整预测新模型版本的生产行为。模型是非确定的,同一个 Prompt 在 DeepSeek-V2.5 和 DeepSeek-V3 上的风格、格式偏好都可能不同,更别提处理边界输入时的差异。直接全量切换就是在赌博。
难点三:上下文一致性
用户在一次长对话里,中途遇到 flag 变更会感知到行为跳变。如果 A/B 测试的 flag 在会话级别不固定,用户可能在同一个对话里前几轮用的是模型 A 的风格,后几轮变成模型 B 的风格。这是很恶劣的体验问题。
难点四:成本控制
高能力模型(比如 DeepSeek-V3、Qwen-Max)比基础模型贵 5-20 倍。你希望高价模型只对付费用户或高优先级请求开放,免费用户走轻量模型。这本质上就是一个 flag 的定向投放问题。
难点五:功能债和 flag 技术债
LLM 功能迭代快,一个特性从实验到稳定可能只要两周。但 flag 如果不清理,会在代码里越堆越多。GrowthBook 的工程博客里提到,很多 AI 团队 6 个月后有 30% 的 flag 处于"不知道能不能删"的状态。
Feature Flag 的基础类型:LLM 控制面的五种 Flag
在 LLM 应用里,不是所有 flag 都一样。根据控制目标,我把它们分成 5 类:
1. Prompt Flag
最直接的 flag。flag 的值是一个 string 或 enum,决定用哪个 Prompt 变体。
typescript
// flag value: "v1" | "v2_friendly" | "v2_strict"
const promptVariant = await flagClient.getStringValue(
'customer-service-prompt',
'v1', // default
{ userId: user.id, tier: user.tier }
);
const systemPrompt = PROMPT_REGISTRY[promptVariant];
Prompt Flag 的 key 指向 Prompt Registry(数据库、配置文件、或 S3),而不是直接在 flag 里存 Prompt 文本。这样 Prompt 内容本身还可以单独更新,flag 只决定用哪个版本。
2. Model Router Flag
flag 决定把请求发到哪个模型。这是成本控制和 A/B 测试的核心工具。
python
model_choice = flag_client.get_string_value(
flag_key='llm-model-tier',
default='deepseek-chat',
evaluation_context={
'user_id': user_id,
'subscription_tier': subscription_tier,
'request_type': request_type,
}
)
response = client.chat.completions.create(
model=model_choice,
messages=messages
)
通过 evaluation_context 里的 subscription_tier,付费用户自动路由到 Qwen-Max,免费用户走 mini 模型,代码逻辑不变。
3. Behavior Flag
控制 Agent 的行为属性:工具调用的权限范围、推理步数、输出格式、是否允许搜索外部数据源。
typescript
const agentConfig = await flagClient.getObjectValue(
'agent-behavior-config',
{
maxToolCalls: 3,
allowWebSearch: false,
outputFormat: 'markdown',
reasoningDepth: 'standard'
},
{ tenantId: tenant.id, environment: env }
);
Behavior Flag 的 value 是 JSON 对象,一次 flag 评估决定一组行为参数。
4. Kill Switch
最简单但最重要的 flag------一个 boolean,切到 false 时立刻把 AI 功能关掉,流量转到非 AI 的降级路径。
python
ai_enabled = flag_client.get_boolean_value(
flag_key='ai-assistant-enabled',
default=True,
evaluation_context={'region': request.region}
)
if not ai_enabled:
return fallback_rule_based_response(user_input)
# 正常走 LLM 路径
response = await llm_client.complete(prompt, user_input)
Kill switch 的响应时间目标是 <1 分钟(flag provider 推送新值)+ 下一次请求立刻生效(服务不需要重启)。这是其他手段(热更新配置、Canary 回滚)无法替代的优势。
5. Cost Gate
当 token 用量超过阈值时,自动降级到轻量模型。这不是一个纯 flag,但可以用 flag 来做动态的成本控制策略切换。
typescript
// flag value: "premium" | "standard" | "economy"
// 由 cost-control 服务根据当前小时用量动态更新
const costTier = await flagClient.getStringValue(
'llm-cost-tier',
'standard',
{ tenantId: tenant.id }
);
const modelMap = {
premium: 'DeepSeek-V3',
standard: 'Qwen-Plus',
economy: 'deepseek-chat'
};
const model = modelMap[costTier];
实战:用 OpenFeature SDK 给 LLM 调用加 Flag
OpenFeature 是 CNCF 维护的 Feature Flag 标准化 SDK 接口,支持切换 provider(LaunchDarkly、GrowthBook、Flagsmith、Unleash 都有适配)而不改应用代码。
以下是一个完整的 Node.js 示例,演示如何在 LLM 调用前做 flag 评估:
typescript
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagsmithProvider } from '@openfeature/flagsmith-provider';
import OpenAI from 'openai' // 兼容 OpenAI 协议的 SDK,可接入国产大模型;
// 初始化 OpenFeature provider(只做一次)
await OpenFeature.setProviderAndWait(
new FlagsmithProvider({ environmentKey: process.env.FLAGSMITH_ENV_KEY! })
);
const flagClient = OpenFeature.getClient('llm-service');
export async function handleUserMessage(
message: string,
context: { userId: string; tenantId: string; subscriptionTier: string }
) {
// 1. 在 LLM 调用前评估所有 flag
const evalCtx = {
targetingKey: context.userId,
tenantId: context.tenantId,
subscriptionTier: context.subscriptionTier,
};
const [modelChoice, promptVariant, aiEnabled] = await Promise.all([
flagClient.getStringValue('llm-model', 'deepseek-chat', evalCtx),
flagClient.getStringValue('system-prompt-variant', 'v1', evalCtx),
flagClient.getBooleanValue('ai-assistant-enabled', true, evalCtx),
]);
// 2. Kill switch:立刻降级
if (!aiEnabled) {
return { source: 'rule-based', content: getRuleBasedResponse(message) };
}
// 3. 从 Prompt Registry 拿实际 Prompt 内容
const systemPrompt = await promptRegistry.get(promptVariant);
// 4. 正常 LLM 调用
const openai = new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: process.env.DEEPSEEK_API_KEY });
const completion = await openai.chat.completions.create # 国产大模型兼容 OpenAI 协议({
model: modelChoice,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: message }
]
});
return {
source: 'ai',
model: modelChoice,
promptVariant,
content: completion.choices[0].message.content
};
}
关键点:所有 flag 评估在 LLM 调用之前并发完成,不在 LLM 内部做判断,LLM 看到的 Prompt 和 model 参数都是已经评估好的。
Python 版本
python
import asyncio
from openfeature import api
from openfeature.provider.flagsmith import FlagsmithProvider
from openai import AsyncOpenAI # 使用兼容 OpenAI 协议的国产大模型接口
api.set_provider(FlagsmithProvider(environment_key=os.environ['FLAGSMITH_ENV_KEY']))
client = api.get_client("llm-service")
openai = AsyncOpenAI(base_url='https://api.deepseek.com/v1', api_key=os.environ['DEEPSEEK_API_KEY'])
async def handle_user_message(message: str, user_id: str, subscription_tier: str):
ctx = {"targetingKey": user_id, "subscriptionTier": subscription_tier}
# 并发评估所有 flag
model_choice, prompt_variant, ai_enabled = await asyncio.gather(
asyncio.to_thread(client.get_string_value, "llm-model", "deepseek-chat", ctx),
asyncio.to_thread(client.get_string_value, "system-prompt-variant", "v1", ctx),
asyncio.to_thread(client.get_boolean_value, "ai-assistant-enabled", True, ctx),
)
if not ai_enabled:
return {"source": "rule-based", "content": get_rule_based_response(message)}
system_prompt = await prompt_registry.get(prompt_variant)
response = await openai.chat.completions.create # 国产大模型兼容 OpenAI 协议(
model=model_choice,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": message}
]
)
return {
"source": "ai",
"model": model_choice,
"content": response.choices[0].message.content
}
5 个生产陷阱
这是最有价值的部分。下面这些错误模式在实际项目里反复出现。
Trap 1:在 LLM 内部判断 flag(最危险)
python
# 错误:把 flag 判断交给 LLM
system_prompt = """
你是客服助手。
如果用户是 VIP 用户,使用更礼貌的语气。 # ← 这不是 flag,这是给 LLM 的指令
"""
这个错误看起来无害,实际上非常危险。LLM 对"VIP 用户"的判断是概率性的,无法保证一致性,也无法追踪。你以为在做定向行为,实际上在做随机行为。
正确做法:用真实的 flag 评估决定 Prompt 内容,不要在 Prompt 里描述"什么情况下怎么做"然后寄希望于 LLM 自己判断。
Trap 2:Flag 粒度太粗
typescript
// 错误:整个 AI 功能只有一个 flag
const aiEnabled = await flagClient.getBooleanValue('ai-enabled', true, ctx);
当一个 flag 控制整个 AI 功能时,一旦某个细粒度的 Prompt 实验出了问题,你只能选择全关(影响所有用户)或全开(继续暴露问题)。
正确做法:按功能子集拆分 flag------ai-search-enabled、ai-draft-enabled、ai-summary-enabled 分开控制。实验 flag 和 Kill Switch flag 不是同一个。
Trap 3:会话级一致性被破坏
python
# 错误:每次请求重新评估 flag,没有 session 固定
async def handle_message(session_id: str, message: str, user_id: str):
model = await flag_client.get_string_value('llm-model', 'default', {'userId': user_id})
# ← 如果 flag 值在会话中途更新,前几轮和后几轮走的是不同模型
用户在对话第 3 轮时遇到模型切换,会感知到风格突变。在涉及上下文记忆的 Agent 里,这会直接导致逻辑混乱。
正确做法:在会话开始时做一次 flag 评估,把结果固定在 session context 里,同一会话内不再重新评估。
python
class Session:
def __init__(self, user_id: str):
self.user_id = user_id
# 会话初始化时固定 flag 评估结果
self.model = flag_client.get_string_value('llm-model', 'default', {'userId': user_id})
self.prompt_variant = flag_client.get_string_value('prompt-variant', 'v1', {'userId': user_id})
async def handle_message(self, message: str):
# 使用固定值,不重新评估
return await llm_call(self.model, self.prompt_variant, message)
Trap 4:Flag 生命周期没人管
bash
# 6 个月后 flag 列表的常见状态
$ flag list
ai-new-prompt-experiment # 谁的实验?什么时候全量还是删掉?
ai-v3-model-test # v3 是什么?现在在测?
ai-feature-2025-11-launch # 2025-11 已经过去了,这个 flag 还有效吗?
ai-emergency-kill-switch # 和主 kill switch 有什么区别?
废弃 flag 不清理会带来两个问题:一是每次请求都在评估无效 flag(增加延迟),二是没人知道能不能删,最终演变成技术债。
正确做法:
- 每个 flag 创建时强制填写 owner、有效期(defaulted to 30 天)、目的描述
- 在 CI 里加一个 lint 步骤检查代码里引用的 flag 是否都在 flag 配置里有对应声明
- 每 2 周 review 一次待清理 flag 列表
Trap 5:Cost Gate 没做 per-user/per-tenant 隔离
python
# 错误:基于全局用量做 cost gate
total_tokens_this_hour = metrics.get('global_token_usage_1h')
if total_tokens_this_hour > THRESHOLD:
model = 'deepseek-chat' # 全局降级,所有用户都受影响
全局 cost gate 会出现「一个高消耗租户把所有人拖下水」的问题。正确的做法是 per-tenant 隔离:
python
# 正确:per-tenant cost gate
tenant_tokens_this_hour = metrics.get(f'token_usage_1h:{tenant_id}')
tenant_quota = quota_service.get_hourly_quota(tenant_id)
if tenant_tokens_this_hour > tenant_quota * 0.9:
# 只降级这个 tenant,不影响其他人
model = 'deepseek-chat'
Kill Switch 的工程设计
Kill switch 是 LLM 应用里最高优先级的 flag,需要单独设计触发路径和降级逻辑。
触发条件
不要只有人工触发一条路径:
| 触发方式 | 描述 | 响应时间目标 |
|---|---|---|
| 人工翻转 | 工程师在 flag dashboard 直接关掉 | <1 分钟(flag 推送) |
| 自动熔断 | 错误率/异常输出率超阈值自动触发 | <30 秒(监控→flag 写入) |
| 成本告警 | 单小时 token 费用超预算自动触发 | <2 分钟(账单 API 轮询) |
| 合规触发 | 检测到违禁内容输出,自动隔离 | <10 秒(在线检测) |
降级路径设计
kill switch 关掉之后,流量要去哪里?这个路径在 kill switch 触发之前就要设计好并测试过:
typescript
async function handleWithFallback(
message: string,
context: EvalContext
): Promise<Response> {
const aiEnabled = await flagClient.getBooleanValue(
'ai-assistant-kill-switch',
true,
context
);
if (!aiEnabled) {
// 降级路径 1:规则引擎
const ruleResult = await ruleEngine.match(message);
if (ruleResult) return { source: 'rules', content: ruleResult };
// 降级路径 2:FAQ 检索
const faqResult = await faqSearch.query(message);
if (faqResult.score > 0.8) return { source: 'faq', content: faqResult.answer };
// 降级路径 3:人工转接
return {
source: 'human-handoff',
content: '当前 AI 服务暂时不可用,已转接人工客服,请稍候。'
};
}
return await callLLM(message, context);
}
降级路径需要定期演练(混沌工程),不能只在代码里写好但从不触发。
Kill Switch 的监控闭环
scss
[LLM 异常检测]
↓ (错误率 > 5% / 有害输出检测)
[自动写入 flag provider: ai-enabled = false]
↓ (flag 推送 <30s)
[所有实例下一次请求走降级路径]
↓
[告警发到 on-call]
↓
[人工确认、定位、修复]
↓
[手动恢复: ai-enabled = true]
↓
[灰度恢复:先 5%,再 50%,再全量]
Flag 生命周期管理
一个 flag 从创建到删除,应该有明确的状态机。
scss
created (实验中)
→ rolled_out (全量)
→ deprecated (代码里已清理引用)
→ archived (从 flag provider 删除)
→ abandoned (实验失败,已回滚)
→ archived
最容易被忽略的是 deprecated → archived 这个步骤。很多团队把 flag 从代码里删了,但 flag provider 里还留着,等到有人问"这个 flag 还有效吗"时已经没人记得了。
在 CI 里加一个检查:
bash
# ci/check-flags.sh
# 检查 flag provider 里的 flag,是否都在代码里有引用
# 如果某个 flag 在 provider 里存在但代码里没有引用 → 警告,可能是遗忘的废弃 flag
另一个好习惯是给每个 flag 加 metadata:
json
{
"key": "ai-new-summarizer-v2",
"owner": "ai-platform-team",
"created_at": "2026-08-01",
"expires_at": "2026-09-01",
"purpose": "Test new summarizer prompt vs v1, target: response length -20%",
"rollout_plan": "5% → 25% → 50% → 100% over 2 weeks",
"success_metric": "CSAT score + p95 latency",
"kill_switch": false
}
对比:Feature Flag vs Canary Deploy vs Config Hot Reload
这三种机制经常被混淆,实际上解决不同维度的问题:
| 维度 | Feature Flag | Canary Deploy | Config Hot Reload |
|---|---|---|---|
| 控制粒度 | 单个功能/行为,可按用户定向 | 整个服务版本,按流量比例 | 服务级配置(无用户区分) |
| 响应速度 | <1 分钟(flag 推送) | 5-15 分钟(重新部署) | <1 分钟(配置重载) |
| 用户定向 | 支持(按 user_id/tenant/属性) | 不支持(按机器/流量比例) | 不支持 |
| 代码变更 | 不需要 | 需要(新版本) | 不需要 |
| 最适合 | Prompt 实验、模型 A/B、kill switch | 重大架构变更、新模型版本 | 模型参数、超时配置、阈值 |
| 审计记录 | flag 变更历史 | 部署记录 | 配置变更日志 |
在实际系统里,三者组合使用:
- Canary Deploy 做新版本 LLM 服务本体的灰度(代码层面的变更)
- Config Hot Reload 做参数级别的动态调整(temperature、max_tokens、timeout)
- Feature Flag 做功能级别的定向控制(对特定用户开启/关闭 AI 功能、Prompt 变体实验)
结论
LLM 应用的 Feature Flag 不是普通工程里 flag 的简单套用,它要处理的控制对象(Prompt、模型、AI 行为)本身就是非确定的,这带来了额外的复杂性。
把本文的核心 checklist 整理如下:
实施清单
- Flag 评估在 LLM 调用之前,不在 Prompt 里做"条件判断"
- 按功能粒度拆分 flag,Kill Switch 和实验 flag 分开
- 会话级 flag 固定:会话开始时评估,不在对话中途重新评估
- 每个 flag 有 owner + 过期时间 + 目的描述
- Kill Switch 有降级路径,且降级路径经过测试
- Cost Gate 做 per-tenant 隔离,不做全局降级
- 定期清理废弃 flag(每 2 周 review,CI 检查引用)
- Kill Switch 支持自动触发(不只是人工操作)
LLM 应用的发布速度已经超过了大多数团队的控制面建设速度。DORA 2025 数据显示,AI 工具让开发者合并 PR 的速度提升了 60%------但控制面如果没有跟上,速度就变成了风险放大器。
Feature Flag 不会让 LLM 应用变得可预测,但它让出问题时的「影响收窄」和「快速恢复」变得可能。这才是它在 LLM 工程栈里真正的价值所在。
参考资料
- GrowthBook: Feature flags in AI-led development (Jun 2026)
- Flagsmith: Feature Flags for AI Companies
- Stanford HAI: AI Index Report 2026
- DORA: State of AI-assisted Software Development 2025
- OpenFeature: CNCF Feature Flag Standard