你会不会也有这种感觉:大模型的能力明明很强,可一旦接进自己的项目,效果立刻像换了一个模型。
这是过去一年里,我在大量开发者社区和实际项目里反复听到的问题。很多人把大模型当成一台“自动生成机”,觉得把需求往对话框里一扔,答案就会自己跳出来。但真实落地时你会发现,同样的模型,有人调出来的是干净的结构化数据,有人调出来的是大段没什么用的散文,还有人直接收到一条报错。差别不在模型,而在“召唤”它的方式。
“The Lamp and the Genie”这个标题,就是在说这件事:大模型是灯里的精灵,能力很强;而你的提示词、上下文、工具调用方式,就是那盏灯。灯磨得对不对,直接决定精灵是帮你实现愿望,还是给你制造一堆麻烦。本文就以这个隐喻为线索,梳理大模型应用开发中最关键、也最容易被低估的一段能力:提示词工程与上下文工程。我会结合一个真实任务——把用户自然语言反馈转成结构化投诉工单,完整走一遍从裸 Prompt 到可信输出的过程,并给出的工程化建议。
1. 从“神灯与精灵”说起:大模型为什么经常用不好?
先讲一个很典型的场景。某个团队接到一个需求:把用户在 App 里的投诉留言自动分类,比如“服务态度差”“等待时间过长”“退款问题”“其他”。业务方希望拿到结构化结果,最好能直接进工单系统。
开发同学的第一反应通常是:“这还不简单?直接让大模型输出 JSON 就行。”于是他写了一句提示词:
请把下面的用户留言分类,并输出 JSON。 用户留言:等了半小时没人接待,前台态度还很差,最后也没解决问题。结果模型返回的可能是这样的内容:
好的,根据您的留言,我将它分类为“服务态度差”。同时,我注意到用户还提到了等待时间过长,所以也可以考虑该问题涉及多个类别。另外,从语气来看,用户情绪比较不满,建议优先处理……这段文字本身没有错,但它不是结构化数据。接下来的工序就卡住了:无法直接写入数据库、无法统计、无法进入后续工单流程。于是开发同学开始加规则、写正则、做各种字符串清洗,越写越痛苦。
这个案例非常典型,它说明了一个关键事实:模型不是做不到,而是开发者在“召唤”它的时候,没有给它足够的上下文约束。你把任务想得越简单,模型返回的结果就越“随意”。相反,真正成熟的开发者会像打磨一盏灯一样,仔细设计系统提示词、示例、输出格式和后置校验。这才是大模型应用开发的核心工作:不是从零写逻辑,而是设计一段能稳定驱动模型的外部上下文。
所以本文的第一个判断是:模型能力本身是相对固定的,变量在于你怎么构造请求。同样一个模型,在一个人手里是工具,在另一个人手里是“玩具”,差异几乎都出在请求侧。下面,我们从概念层面把这个差异讲清楚。
2. 提示词工程与上下文工程:核心概念辨析
先澄清两个经常被混在一起的概念:Prompt Engineering(提示词工程)和 Context Engineering(上下文工程)。
提示词工程,指的是设计输入给模型的文本指令,让它更准确地完成目标。它关注的是“怎么说”。比如用“你是一个资深客服运营专家”开场、把问题拆成若干步骤、明确输出格式,这些都属于提示词工程。它的核心工具包括:系统提示词、few-shot 示例、思维链引导、输出格式声明。
上下文工程的范围更大。它关心的是:在模型生成之前,你往请求里放入了哪些信息,以及这些信息以什么结构存在。除了提示词本身,上下文工程还包括外部检索结果、用户历史会话、知识库片段、数据库结构、当前页面状态、工具调用结果等等。它的核心问题是:为了让模型在特定任务上稳定发挥,我们应该给它看什么?先看什么?哪些信息必须放在前面,哪些工具它必须能调用?
为什么这两个概念要分开?因为现代大模型应用的复杂度已经远远超过“写一段提示词”。举例来说,一个客服问答 Agent 的上下文可能包含:
- 系统提示词:角色、任务、约束、语气规范。
- 当前用户的问题。
- 通过向量检索召回的 5 条知识库片段。
- 用户最近 3 轮对话历史。
- 当前登录用户的基础信息。
- 可供调用的工具 API 描述。
这一段“上下文组合”,才是模型真正用来推理的素材。提示词工程解决的是“文字指令好不好”,上下文工程解决的是“信息拼盘对不对”。你可以写出一句完美的系统提示词,但如果知识库字段错误、工具描述含糊、上下文顺序混乱,模型依然会做出错误判断。
下面用一个简单对比来收束:
| 维度 | 提示词工程 | 上下文工程 |
|---|---|---|
| 关注对象 | 指令文本 | 模型所见的一切信息 |
| 核心问题 | 怎么把任务说清楚 | 怎么把任务所需的资料组织好 |
| 常见手段 | 角色设定、few-shot、思维链 | RAG 检索、会话管理、工具调用、状态注入 |
| 失败表现 | 输出格式不对、理解偏差 | 信息缺失、知识错误、工具选择错误 |
| 工程价值 | 提高单次输出质量 | 决定复杂任务能否稳定完成 |
所以我的第二个判断是:单纯追求“提示词技巧”是不够的。你要解决的是信息组织问题,而不是文字排版问题。理解了这一点,你才明白为什么同样一个 Prompt,在不同项目里效果天差地别。
3. 大模型应用开发的整体链路与适用场景
为了不让讨论停留在概念层面,这里把大模型应用开发的完整链路拆出来。不管你是做客服助手、内容总结、信息抽取,还是做 Agent 编排,链路大体都包含六个环节。
第一,任务定义。明确你的输入是什么、输出是什么、验收标准是什么。很多项目一开始就失败,不是因为模型不强,而是因为任务定义模糊。比如“帮我识别用户情绪”听起来简单,但“愤怒”和“失望”的边界在哪里?如果有多个情绪,取第一个还是全部输出?这些都必须写清楚。
第二,上下文构造。这是整个链路的核心。你要把原始输入、系统提示词、外部知识、历史记录组装成一个模型可消费的 messages 结构。顺序、长度、优先级都需要设计。
第三,模型调用。选择模型、设置温度、控制最大 token、决定超时和重试策略。这里的工程问题比大多数人想象的多:高并发、限流、网络抖动、模型升级引发的行为变化,都需要考虑。
第四,输出校验。模型输出只是候选结果,不是最终结论。必须做 JSON 解析、字段校验、枚举值检查。这一步做不到,后面所有环节都是空中楼阁。
第五,后处理与工具调用。如果结果不满足要求,可能是让模型重新生成,也可能是调用外部工具补充数据,再基于工具结果生成最终回答。
第六,存储与展示。把结果落库、同步到工单系统、推送给用户。
在实际项目里,这六个环节的复杂度不同。比如一个“总结用户反馈并分类”的功能,第四和第五步相对简单;而一个“自动处理工单”的 Agent,第五步会变得非常复杂,因为它涉及工具选择、参数提取、结果验证、异常处理。
那么什么场景真正适合用大模型?我的判断是:自然语言到结构化信息的映射,是大模型当前最成熟、性价比最高的落地场景之一。比如用户留言分类、合同关键信息提取、客服会话摘要、代码注释生成。这类任务的特点是:输入是语言,输出也是语言,但需要稳定转化为程序能用的数据。相反,如果任务是精确计算、强实时响应、严格事务控制,那不应该把核心逻辑交给大模型,而是应该用传统代码实现,让大模型只负责理解和生成的部分。
理清这些边界后,我们直接进入实操环节。接下来的章节会围绕“自然语言反馈转结构化工单”这个示例展开,一步步演示如何构造上下文、调用模型、校验输出。
4. 环境准备与前置条件
在开始写代码之前,先把运行环境准备好。本文的示例基于 Python、OpenAI 兼容接口和一个可调用的对话补全 API。不同厂商的 SDK 包名和参数可能略有差异,请以你实际使用的服务商官方文档为准。
开发环境建议如下:
- 操作系统:Windows / macOS / Linux 均可。
- Python:3.10 及以上版本。
- 包管理:pip 或 poetry。
- 模型接口:具备对话补全能力,并支持系统消息。
- 可选:支持 JSON 输出模式的服务商,可以简化解析。
先创建项目目录和虚拟环境:
mkdir lamp-genie-demo cd lamp-genie-demo python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate然后安装依赖。核心只用到一个 OpenAI 兼容 SDK,再加一个用于读取环境变量的 python-dotenv:
pip install openai python-dotenv这里有一个非常值得强调的工程习惯:不要在代码里硬编码 API Key。正确的做法是把 Key 放到环境变量或本地配置文件中,并确保它不会进入 Git 仓库。在项目根目录创建一个.env文件:
OPENAI_API_KEY=your-api-key-here OPENAI_BASE_URL=https://api.example.com/v1 MODEL_NAME=your-model-name然后在项目目录添加一个.gitignore,内容至少包括:
.env venv/ __pycache__/安全配置完成后,我们先跑一个最小连通性测试,确认 SDK、网络和鉴权都正常:
import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.environ["OPENAI_API_KEY"], base_url=os.environ.get("OPENAI_BASE_URL"), ) resp = client.chat.completions.create( model=os.environ["MODEL_NAME"], messages=[ {"role": "user", "content": "请只回复两个字:成功"} ], temperature=0, ) print(resp.choices[0].message.content)如果控制台输出“成功”,说明环境已经可用。这里有几个肉眼容易忽略但实际很关键的细节:
temperature=0是这类结构化抽取任务的推荐设置,目的是降低随机性。base_url不是必填项,如果你直接使用 OpenAI 官方服务,可以省略;如果你使用国内合规服务商或私有化部署网关,就需要填写对应的接口地址。- API Key 千万不要提交到公共仓库。一旦泄露,第一时间去控制台吊销并重新生成。
5. 一个真实任务:从自然语言反馈到结构化结果
接下来我们进入主线任务。假设你正在为一家连锁线下门店做用户反馈分析系统,输入是用户留下的自然语言投诉,输出是一个可以直接写入数据库的结构化工单。
先定义输入输出。输入示例:
等了半小时没人接待,前台态度还很差,最后也没解决问题。期望输出:
{ "category": "service_attitude", "severity": "high", "emotion": "angry", "action_required": "需要客服致歉并跟进", "summary": "用户等待超过半小时,且前台接待态度差,问题未解决", "confidence": 0.87 }字段含义如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| category | string | 投诉分类,枚举:service_attitude, waiting_time, refund, others |
| severity | string | 严重程度,枚举:low, medium, high |
| emotion | string | 用户情绪,枚举:calm, neutral, angry |
| action_required | string | 建议的处理动作描述 |
| summary | string | 一句话问题摘要 |
| confidence | float | 模型对自身判断的置信度,0 到 1 之间 |
这里要特别说明一下confidence的定位:它并不是一个真实的统计概率值,而是模型的主观评估。它不能替代业务规则的优先级判断,但可以用作排序和人工抽检的参考。理解这一点,能避免你在后续使用这个字段时做出错误的业务决策。
现在看一个错误示范。如果提示词只写了“请把用户留言分类”,就像第 1 章展示的那样,输出大概率不是合法 JSON。更糟糕的是,如果你没有在系统提示词里定义枚举值,模型就会自己发明一堆分类,比如“客户服务质量不好”“接待效率低”这种无法直接入库的字符串。分类体系一旦混乱,后续所有统计报表都会变得不可信。
所以,构造上下文时要遵循三个原则:
第一,把任务背景写清楚。你要告诉模型它是谁、在做什么、为谁服务、输出给谁用。
第二,把所有约束写彻底。枚举值有哪些、字段可选还是必填、JSON 里能不能有多余文字、置信度保留几位小数。每一条看似“不重要”的约束,都可能直接影响输出质量。
第三,给出少量示例。示例的作用不是让模型“学会”业务,而是示例一两个充满歧义的边界情况,引导模型在类似情况下选择正确的处理方式。
基于这三个原则,下一章给出完整可运行的代码实现。
6. 完整代码实现与运行验证
下面这段代码是完整的实现。它包含系统提示词、few-shot 示例、模型调用、JSON 解析、字段校验和兜底处理。你复制之后,只需要替换服务和模型配置即可运行。
import json import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.environ["OPENAI_API_KEY"], base_url=os.environ.get("OPENAI_BASE_URL"), ) system_prompt = """ 你是一位资深客服工单分析师。你的任务是从用户投诉留言中提取结构化信息,生成一份可用于工单系统的 JSON 结果。 请严格遵守以下约束: 1. 只输出一个 JSON 对象,不要包含任何解释、前缀或 Markdown 代码块标记。 2. category 只能是以下枚举值之一:service_attitude、waiting_time、refund、others。 3. severity 只能是 low、medium、high 之一。判断标准:涉及财产损失或升级冲突为 high;明确表达不满但未升级为 medium;一般性反馈为 low。 4. emotion 只能是 calm、neutral、angry 之一。 5. summary 必须是一句简洁的中文描述,不超过 40 字。 6. action_required 必须给出明确的处理建议,不能为空。 7. confidence 是 0 到 1 之间的小数,保留两位小数,表示你对本次判断的把握程度。 8. 如果同一段留言涉及多个问题,只选择最重要的一类作为 category。 示例输入:门店打烊时间写错了,白跑一趟。 示例输出:{"category": "others", "severity": "medium", "emotion": "neutral", "summary": "用户因门店营业时间信息错误白跑一趟", "action_required": "修正门店信息并致歉", "confidence": 0.78} """ def parse_and_validate(content: str) -> dict: """解析模型输出,并做基础校验。如果失败,抛出异常。""" text = content.strip() # 去掉可能出现的 markdown 代码块标记 if text.startswith("```json"): text = text.removeprefix("```json").strip() if text.startswith("```"): text = text.strip("`").strip() data = json.loads(text) required_fields = { "category": str, "severity": str, "emotion": str, "summary": str, "action_required": str, "confidence": float, } for field, field_type in required_fields.items(): if field not in data: raise ValueError(f"缺少字段: {field}") if not isinstance(data[field], field_type): raise ValueError(f"字段类型错误: {field}, 期望 {field_type}") if data["category"] not in {"service_attitude", "waiting_time", "refund", "others"}: raise ValueError(f"非法的 category: {data['category']}") if data["severity"] not in {"low", "medium", "high"}: raise ValueError(f"非法的 severity: {data['severity']}") if data["emotion"] not in {"calm", "neutral", "angry"}: raise ValueError(f"非法的 emotion: {data['emotion']}") data["confidence"] = float(data["confidence"]) return data def classify_feedback(user_input: str) -> dict: resp = client.chat.completions.create( model=os.environ["MODEL_NAME"], messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input}, ], temperature=0, ) content = resp.choices[0].message.content return parse_and_validate(content) if __name__ == "__main__": test_input = "等了半小时没人接待,前台态度还很差,最后也没解决问题。" try: result = classify_feedback(test_input) print(json.dumps(result, ensure_ascii=False, indent=2)) except Exception as e: print(f"处理失败: {e}")运行方式很简单:
python classify_feedback.py一个可行的输出如下:
{ "category": "service_attitude", "severity": "high", "emotion": "angry", "summary": "用户等待超过半小时,且前台接待态度差,问题未解决", "action_required": "由店长致歉并回访跟进处理方案", "confidence": 0.85 }这段代码有几个关键设计值得展开说明。
第一,系统提示词里明确要求“只输出一个 JSON 对象,不要包含任何解释、前缀或 Markdown 代码块标记”。这是为了最大限度避免后端解析失败。即便这样,解析函数里仍然做了防御性处理,能去掉常见的 markdown 包裹标记,这属于“前端约束 + 后端兜底”的经典组合。
第二,temperature=0让模型在每次相似输入下尽量输出稳定结果。虽然不能做到绝对确定性,但对于分类和抽取任务,这已经能显著降低随机波动。
第三,JSON 校验不是只判断“能不能解析”,还检查了字段是否存在、类型是否正确、枚举值是否合法。这样做的目的是把错误拦截在数据落库之前。实际生产环境里,你还可以在action_required上进行长度检查,或者在summary上做字数控制。
如果你使用的服务商支持 JSON 输出模式,可以在调用时增加一个参数,以进一步减少非 JSON 输出:
resp = client.chat.completions.create( model=os.environ["MODEL_NAME"], messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input}, ], temperature=0, response_format={"type": "json_object"}, )需要注意:不是所有服务商都支持这个参数。如果不支持,请把它去掉,继续靠系统提示词约束和解析兜底。这也是我们不能只依赖某个单一特性的原因,工程链路要保证在多种环境下都能降级工作。
7. 效果验证:怎么判断输出是“真的可用”而不是“看起来不错”
很多团队走到上一步就宣布“功能完成”了,这是错误示范。你对着一条测试数据看着很顺眼,不代表模型在 100 条真实数据上也能稳定输出。效果验证必须超越“肉眼看一眼”,最起码要做三件事。
第一件事,格式通过率。准备 20 条真实或接近真实的用户留言,逐条调用接口,统计能通过parse_and_validate的比例。如果通过率低于 95%,说明提示词约束还不够,需要补示例或加强系统提示词。这里真正容易踩坑的地方是:模型偶尔会在 JSON 后面追加一句解释性文字,或者把枚举值写成中文,比如"服务态度差",这种问题在 20 条测试里可能只出现一次,但线上跑起来会很致命。
第二件事,分类正确率。格式通过只代表“能解析”,不代表“分类正确”。你需要人工给 20 条测试数据打上预期分类,再和模型结果对比。比如“退款到账太慢”和“不给开发票”都可能属于 refund,但模型可能把前者分到 waiting_time,因为语义上它和时间有关。这类歧义问题,必须通过更精细的枚举值定义和更多的 few-shot 示例来解决。
第三件事,边界案例覆盖。真实世界的用户留言非常粗糙,常见边界包括:
- 一段留言里包含多个投诉点,模型应该选最重要的一个,而不是全部输出。
- 用户使用了反讽语气,模型很容易误判情绪。
- 留言里包含地址、电话、姓名等敏感个人信息,需要在进入模型之前脱敏,或在输出后处理。
- 输入是非中文内容,需要明确模型应该如何处理,是翻译还是拒绝分类。
为了把人工评估沉淀下来,我建议建立一个小型评测表,用表格记录每次测试的结果。
| 维度 | 评估方式 | 达标线 |
|---|---|---|
| 格式通过率 | 自动解析通过数 / 总测试数 | 95% 以上 |
| 分类准确率 | 人工标注对比模型结果 | 90% 以上 |
| 枚举合法率 | 输出字段是否都在定义范围内 | 100% |
| 摘要可读性 | 人工判断是否语义通顺 | 95% 以上 |
| 敏感信息泄漏 | 输出是否包含手机号、地址等 | 0 起 |
如果达不到标准,优先调整系统提示词中的枚举定义和示例,其次是增加预处理和后处理规则,最后才考虑更换更强或更大的模型。要记住:换模型是成本最高、最不可控的方案,先用上下文工程把当前模型压榨到位才是更高效的做法。
8. 常见问题与排查思路
大模型应用出问题的时候,排查思路和传统应用不太一样。传统系统里,错误通常是确定的,你可以按调用栈一路追下去;但在大模型应用里,很多时候没有报错,只是输出“不对”。下面把最常见的问题整理成一张排查表,供你作为参考。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 输出是散文,不是 JSON | 系统提示词没有明确输出格式 | 查看请求 messages 中 system 内容 | 增加“只输出 JSON 对象”和示例 |
| JSON 解析偶尔失败 | 模型在 JSON 外输出解释或 markdown 标记 | 打印原始返回内容 | 解析前做清洗,或使用 response_format |
| 分类枚举值不固定 | 系统提示词没有给出枚举定义 | 检查输出集合,统计非法值 | 在 system 中给出枚举和例句 |
| 同一段输入多次调用结果不同 | 温度过高或上下文有随机性 | 对比多次输出 | 将 temperature 设置为 0 |
| 摘要过长或过短 | 没有字数约束或约束不生效 | 检查 summary 输出长度 | 增加“不超过 40 字”等硬约束 |
| 输入包含用户联系方式,输出也带出 | 没有做输入脱敏 | 检查原请求日志 | 上游脱敏,或输出侧使用 PII 过滤器 |
| 上下文长度超限 | RAG 片段或对话历史过长 | 查看 token 用量统计 | 截断、压缩、分段检索 |
| 成本超预期 | few-shot 示例太长、并发太高 | 统计每次调用的 token 数 | 精简示例,优先用小模型分流 |
| 某类问题总是分错 | 分类定义边界模糊或示例不足 | 对错误案例聚类分析 | 补充对应示例,细化枚举定义 |
| 客户在回答里被“套话”或诱导输出危险指令 | 提示词注入 | 把用户输入与系统指令隔离 | 加输入过滤、权限限制和输出审查 |
如果你遇到的问题是“解析失败”,第一步永远不是改代码,而是把模型返回的原始字符串完整打出来,看它到底是什么形态。很多时候你以为模型输出的是标准 JSON,实际上它在外面包了```json标记,或者中间夹了注释,导致标准 JSON 解析器直接抛异常。先看原始输出,再决定是改提示词、加清洗还是换接口参数。
如果遇到分类不准,优先检查是不是枚举定义和真实用户表达存在认知偏差。比如“退款”这个分类,真实用户可以表达为“退钱”“没到账”“发票不对”,甚至“再也不来了”。枚举定义要能覆盖这些口语化表达。如果你的系统提示词里只写了“refund:退款”,模型确实可能漏分。正确做法是写出更详细的分类说明,必要时给每个枚举配一个正例和一个反例。
这里还有一个容易忽略的问题:模型输出里的confidence不可靠。它没有经过校准,同一个模型在不同任务上的自信程度波动很大。不要拿它作为自动判定的硬指标,更不要用阈值直接拦截工单。更稳妥的做法是把它用作人工抽检排序条件:抽检时优先看低置信度的记录。
9. 最佳实践:从 Demo 到生产环境的工程建议
很多项目能跑通 Demo,但一上生产就崩。原因通常不是模型不行,而是工程化程度不够。下面这六条建议,是任何大模型落地项目都值得提前想清楚的。
第一,把提示词当成代码来管理。提示词不是“文案”,它是功能代码的一部分。它应该和代码一起进入 Git 仓库,跟随版本发布。不要直接在线上控制台里改提示词,否则你无法回溯“这一版效果变好,到底改了什么”。推荐在项目里维护prompts/目录,每个任务一个文件,并写清楚变更记录。
第二,评测集先行。上线一个新功能之前,先准备一个包含典型输入、边界输入和错误输入的小型评测集。以后每次改提示词、换模型、升级 SDK,都先跑一遍评测集。这样能有效防止“修好一个 case,弄坏一片 case”的情况。没有评测集的大模型开发,就像没有单测的传统开发,越到后期风险越大。
第三,设计降级与兜底。模型调用是典型的不可靠依赖,它可能会超时、限流、返回非 JSON、甚至服务不可用。生产系统必须为这些情况设计降级路径。最常见的做法是:解析失败时返回默认分类others,记录异常日志,并进入人工处理队列;而不是让整个接口直接 500。记住:在核心链路里,模型输出永远只是候选结果,最终决策要由你的代码来拍板。
第四,做好安全边界。大模型应用的安全问题,比传统 Web 应用多一层提示词注入的风险。攻击者可能故意在用户输入里构造“忽略之前的指令”之类的注入语句,试图让模型执行非预期操作。防御手段包括:在系统提示词中声明用户输入只是待处理内容,不是指令;对敏感操作设置权限校验;输出侧增加内容审核;日志里不要记录完整敏感信息。
第五,成本要算细账。每多一条 few-shot 示例,都会增加输入 token 数;每次调用多带 2000 个 token,在日均百万次调用的规模下,成本差异非常可观。生产环境的建议是:用“小模型大流量、大模型小流量”的分层策略。比如简单分类走轻量模型,复杂推理和 Agent 任务才走大参数模型。同时使用缓存,对相似输入做结果复用。
第六,可观测性必须从第一天就建立。每次模型调用都要记录:timestamp、model、prompt_hash、input_tokens、output_tokens、latency_ms、status、error_code、parsed_success。有了这些数据,你才能回答“模型为什么今天变慢了”“为什么某类输入解析成功率下降了”。没有日志的大模型应用,排错时只能靠猜。
下面给一个简单的日志记录片段,你可以把它集成到调用函数中:
import time import hashlib import json def classify_feedback_with_log(user_input: str, call_id: str) -> dict: start = time.time() resp = client.chat.completions.create( model=os.environ["MODEL_NAME"], messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input}, ], temperature=0, ) latency_ms = int((time.time() - start) * 1000) content = resp.choices[0].message.content tokens = resp.usage try: result = parse_and_validate(content) status = "success" parsed_success = True except Exception as e: result = {"category": "others"} status = f"parse_error: {e}" parsed_success = False print(json.dumps({ "call_id": call_id, "prompt_hash": hashlib.sha256(system_prompt.encode()).hexdigest()[:12], "input_tokens": tokens.prompt_tokens, "output_tokens": tokens.completion_tokens, "latency_ms": latency_ms, "status": status, "parsed_success": parsed_success, "category": result.get("category"), }, ensure_ascii=False)) return result这个示例展示了记录日志的最小闭环。真实系统里,你应该把这条 JSON 写入日志存储或监控系统,而不是只打印到控制台。
10. 结束前再说一次“灯与精灵”
回到文章标题。大模型擅长的事,不是“帮你完成任务”,而是“在你提供了充分上下文时,帮你生成完成任务所需的文本或结构”。它就像一个被装在灯里的精灵,力量巨大,但不会主动理解你的业务、你的数据规范、你的字段定义。所有这些,都需要你在召唤它之前准备好。
换句话说,提示词工程和上下文工程不是某种花哨的文案技巧,而是大模型时代的“接口设计”。你如何定义系统提示词,就是如何定义模型的行为基线;你如何组织 few-shot 示例,就是如何引导模型处理边界场景;你如何设计 JSON Schema 和校验逻辑,就是如何让模型输出可进入下一道工序。这些能力的总和,决定了一个模型能力相同的团队,交付出来的产品是完全不同等级的。
如果你只记住一件事,那就记住这句话:先定义问题,再设计上下文,最后用校验兜底。不要寄希望于模型“懂你的意思”。把任务边界写清楚,把输出结构锁死,把失败路径准备好,大模型应用才能真正走向稳定。
这篇文章里,我们从一个用户投诉分类任务出发,走完了任务定义、提示词设计、上下文组织、代码实现、效果验证、问题排查和工程化部署的完整路径。你接下来可以做的,是找一条真实业务数据,把这里的代码改成你自己的分类体系,跑一遍,然后建立你的第一个评测集。这个过程做完,你会发现自己对这个“精灵”的理解,已经比大多数停留在“调用 API”阶段的人深了一层。