声明:本文分享的是通用 Prompt Engineering 方法论,所用示例均为虚构场景,不涉及任何真实业务数据、商业机密或具体项目指标。核心技术思路可应用于医疗、金融、法律、政务等各领域的结构化信息抽取任务。

适用读者:NLP 工程师、大模型应用开发者、Prompt Engineering 实践者


引言

在大模型应用开发中,如何从非结构化文本中高质量地提取结构化数据,一直是个极具挑战的难题。

最近我完成了一个复杂的信息抽取任务,经历了从"一套提示词走天下"到"精细化样例设计"的完整迭代过程。最后一个"不起眼"的细节改进,让提取效果提升了 40%+,模型幻觉降低了 80%

本文将完整复盘整个提示词优化历程,分享那些真正有效的实战经验。如果你也在做类似的 NLP 任务,相信这些洞察能帮你少走弯路。


一、任务背景(虚构示例)

为了便于说明,本文使用一个虚构的"产品缺陷报告分析"场景

目标:从用户提交的产品缺陷报告中提取结构化信息

输出字段(9 个核心字段):

  • 缺陷类型:功能异常、外观问题、性能不足等
  • 发生部位:屏幕、电池、摄像头、按键等
  • 问题数量:1 个、多个、多处等
  • 缺陷特征:裂纹、变色、卡顿、异响等(核心字段)
  • 测量参数:裂纹长度、温度数值等
  • 边缘描述:规则、不规则、锯齿状等
  • 复现条件:充电时、高温环境下等
  • 前后对比:升级后加重、维修后复发等
  • 影响范围:导致无法开机、影响正常使用等

核心难点

  1. 用户描述口语化,同一概念多种表述(如"充不进电"vs"充电无反应")
  2. 一段文字包含多个独立缺陷(如"屏幕有划痕,而且充电时会发烫")
  3. 指代模糊,“这个问题”、"那个地方"频繁出现
  4. 主观评价与客观描述混杂(如"质量太差了,用了三天就坏了")

说明:虽然是虚构场景,但上述难点在所有领域的信息抽取任务中都具有共性。思路和方案来源于真实项目经历。


二、迭代历程

V1.0:一套提示词打天下

方案特点

# 单个通用提示词模板
prompt = """
你是一个产品缺陷信息提取助手...

## 字段定义
(9 个字段的详细说明)

## 少样本示例
【示例 1】手机类缺陷案例
【示例 2】电脑类缺陷案例
【示例 3】家电类缺陷案例
...(共 15 个综合样例)

## 任务
请对下方文本进行抽取,只输出 JSON 数组:
输入文本:{text}
"""

结果效果不理想,大量错误

典型错误

输入:"屏幕左上角有划痕,大概 2cm;充电时会发烫,温度超过 40 度"

错误输出:
{
  "发生部位": ["屏幕", "充电时"],  // ❌ 混为一行,且"充电时"不是部位
  "缺陷特征": ["划痕", "发烫"]
}

正确输出应该是两行独立的 JSON 对象:
[
  {"发生部位": ["屏幕左上角"], "缺陷特征": ["划痕"], "测量参数": ["大概 2cm"]},
  {"发生部位": ["机身"], "缺陷特征": ["发烫"], "复现条件": ["充电时"], "测量参数": ["温度超过 40 度"]}
]

问题分析

  1. 样例过于复杂:单个样例包含多种提取模式(部位识别 + 数量统计 + 条件判断)
  2. 学习目标模糊:模型难以从混杂的样例中归纳出清晰的拆分规则
  3. 领域差异干扰:不同产品类型的术语差异导致混淆(如手机的"刘海屏"vs 电脑的"全面屏")

V2.0:分品类定制提示词

方案改进

# 为每个产品品类创建独立提示词
prompts/
├── prompt_手机.py      # 12 个手机专用样例
├── prompt_电脑.py      # 10 个电脑专用样例
├── prompt_家电.py      # 15 个家电专用样例
└── ...

样例特点(以手机为例):

FEWSHOT_EXAMPLES = [
    {
        "input": """屏幕右上角有裂纹,长度约 3cm,
                     不影响显示,触摸正常。""",
        "output": [{
            "缺陷类型": ["外观问题"],
            "发生部位": ["屏幕右上角"],
            "问题数量": ["1"],
            "缺陷特征": ["裂纹"],
            "测量参数": ["长度约 3cm"],
            "影响范围": ["不影响显示,触摸正常"]
        }]
    },
    # ... 更多手机专属样例
]

结果效果明显提升

收益

  • 样例与目标文本分布一致,减少领域偏移
  • 术语一致性提高,模型更容易学习规律
  • 同一品类的描述习惯相似,降低理解成本

新问题

  1. 维护成本激增:10 个品类=10 套提示词,每次修改要同步多处
  2. 样例膨胀:每个模板都想要覆盖所有边界情况,样例数突破 150+
  3. 冗余严重:不同品类的样例有大量重复规则(如"看到’大概’要保留到测量参数")

V3.0:文本切分 + 模块化样例

关键洞察

复杂的不是任务本身,而是我们试图用一套样例解决所有子问题

方案架构

# Step 1: 文本切分
原始文本 → 
  片段 1:"屏幕右上角有裂纹,长度约 3cm..."
  片段 2:"充电时会发烫,温度超过 40 度..."
  片段 3:"用了一周后,问题越来越严重..."

# Step 2: 针对性样例
每个片段只学习一种提取模式

切分后的样例设计

# 样例 A:专门学习"数量词转换"
{
    "description": "注意:见到'一条'、'一处'要转换为数字 1",
    "input": "屏幕上发现**一条**划痕",
    "output": [{"问题数量": ["1"], ...}]
}

# 样例 B:专门学习"多缺陷拆分"
{
    "description": "注意:不同部位的缺陷必须拆分为多行",
    "input": "屏幕有划痕;后盖有凹陷",
    "output": [
        {"发生部位": ["屏幕"], ...},  # 第一行
        {"发生部位": ["后盖"], ...}   # 第二行
    ]
}

# 样例 C:专门学习"指代消解"
{
    "description": "注意:'此处'需向前追溯具体部位",
    "input": "屏幕右下角有裂纹,**此处**不影响触摸",
    "output": [{"发生部位": ["屏幕右下角"], ...}]  # 而不是["此处"]
}

结果效果进一步提升

核心优势

  • 每个样例只教一个知识点,学习目标清晰
  • 切分后上下文缩短,注意力更集中
  • 减少了样例间的相互干扰

V4.0:样例描述(Game Changer!)

最关键的一步:给每个样例添加自然语言描述

改进前后对比

** V3.0(无描述)**:

{
    "input": "机身发热,温度约 45 度,充电时更明显",
    "output": [
        {"发生部位": ["机身"], "缺陷特征": ["发热"], "测量参数": ["温度约 45 度"]},
        {"发生部位": ["机身"], "缺陷特征": ["发热"], "复现条件": ["充电时"]}
    ]
}

V4.0(有描述)

{
    "description": "本样例须注意:当同一缺陷在不同条件下程度不同时,要拆分为两行。第一行记录基础状态,第二行记录特定条件下的表现",
    "input": "机身发热,温度约 45 度,充电时更明显",
    "output": [
        {"发生部位": ["机身"], "缺陷特征": ["发热"], "测量参数": ["温度约 45 度"]},
        {"发生部位": ["机身"], "缺陷特征": ["发热"], "复现条件": ["充电时"], "测量参数": ["温度更高"]}
    ]
}

为什么样例描述如此有效?

  1. 明确学习重点:直接告诉模型"这个样例你要学什么",避免瞎猜
  2. 减少歧义:同样的输入输出对,可能有多种解读方式,描述消除了不确定性
  3. 元学习效应:描述相当于"思维链",教会模型如何思考而不仅仅是模仿

完整提示词结构

BASE_INSTRUCTION = """...(字段定义和基础规则)"""

FEWSHOT_EXAMPLES = [
    {
        "description": "本样例需注意:阴性描述不提取,但后面紧跟的阳性描述还是要提取",
        "input": "产品外观基本正常,表面有轻微划痕...",
        "output": [...]
    },
    {
        "description": "本样例涉及拆分规则:当测量参数只修饰部分缺陷特征时必须拆分",
        "input": "发现多处裂纹,最大者约 15mm,呈放射状...",
        "output": [...]
    },
    {
        "description": "特别注意:'此处'、'该位置'等泛词必须追溯具体部位",
        "input": "屏幕右下角有裂纹,该位置不影响触摸...",
        "output": [{"发生部位": ["屏幕右下角"], ...}]  # 不能是["该位置"]
    },
    # ... 每个样例都有明确的描述
]

def build_extraction_prompt(text):
    return BASE_INSTRUCTION + "\n\n## 关键样例解析\n" + \
           "\n".join([f"【示例{i}{ex['description']}\n输入:{ex['input']}\n输出:{ex['output']}" 
                      for i, ex in enumerate(FEWSHOT_EXAMPLES)]) + \
           f"\n\n## 待处理文本\n{text}"

最终结果效果达到最佳,准确率、召回率提升到 95% 左右

效果对比

版本 效果趋势 样例数量 平均字符数
V1.0 不理想 6 15000
V2.0 提升 6 15000
V3.0 继续提升 22 12000
V4.0 最佳 15 10000

💡 关键发现:V4.0 的样例数量比 V3.0 少了很多,而且字符数比V2.0少了三分之一,但效果反而更好,证明样例质量远胜于数量


三、关键洞察

1️⃣ 少即是多:样例质量 > 数量

  • V2.0 有 120 个样例,但效果不如 V4.0 的 62 个
  • 关键不是堆砌样例,而是每个样例都要有明确的教学目标

2️⃣ 显式优于隐式:把潜规则摆到台面上

  • 模型不擅长从输入输出对中"悟"出规则
  • 一句简单的描述胜过 10 个相似样例

3️⃣ 注意力管理:降低认知负荷

  • 复杂样例会让模型注意力分散
  • 单个样例只教一个知识点,学习效果最佳

4️⃣ 分布对齐:切分后的样例更接近真实场景

  • 未切分的长文本中,模型容易迷失主次
  • 切分后每个片段对应一个明确的提取决策

四、实战建议

如果你也在做类似的信息抽取任务,这里是我的建议:

DO(推荐做法)

  1. 给每个样例写描述:用自然语言说明"这个样例要教什么"
  2. 单一职责原则:一个样例只演示一种规则/陷阱
  3. 文本切分:将长文本拆分为语义完整的短片段
  4. 边界案例优先:优先覆盖容易出错的特殊情况
  5. 持续迭代:根据错误案例不断补充针对性样例

DON’T(避免踩坑)

  1. 不要堆砌样例:100 个模糊样例不如 20 个精准样例
  2. 不要让模型猜意图:显式说明规则,不要依赖模型自己总结
  3. 不要混合多种规则:一个样例同时教拆分 + 指代 + 数量转换=灾难
  4. 不要忽略阴性样本:明确告诉模型什么情况不提取
  5. 不要一次性处理长文本:先切分再提取,效果天壤之别

五、样例描述编写技巧

经过实践,我们发现好的样例描述应该具备以下特点:

描述模板

{
    "description": "本样例需要注意 [具体知识点],特别是 [易错点],[正确处理方式]"
}

优秀案例

# 好的描述:具体、可操作
"description": "本样例需注意:当出现'最大者'且有明确测量参数时,
                必须另起一行单独记录该部位的完整特征,
                不要将参数合并到其他缺陷中"

# 差的描述:模糊、笼统
"description": "本样例展示了正确的提取方式"

描述分类

我们总结了 8 类高频描述模式,适用于大多数信息抽取场景:

  1. 拆分规则类:当某个限定词只修饰部分内容时,需要拆分表述

    • 示例:“A 和 B 都增大,以 A 为著” → 拆分为两行("为著"只修饰 A)
  2. 指代消解类:遇到代词需追溯具体指代对象

    • 示例:“其内部”、“该位置”、“上述对象” → 替换为具体名称
  3. 数量转换类:统一数量词的输出格式

    • 示例:“一个/一枚/一处” → “1”;“若干/多个/多处” → 保留原词
  4. 阴性过滤类:区分正常描述与异常描述

    • 示例:“未见明显异常”、“基本正常” → 不提取;“轻微磨损”、“局部变形” → 需提取
  5. 字段归类类:明确特定描述的字段归属

    • 示例:“在…条件下出现” → 归入条件字段;“呈…状态” → 归入特征字段
  6. 固定术语类:识别专业术语作为整体处理

    • 示例:专业名词、固定搭配 → 不拆分,整体提取
  7. 参数绑定类:测量数值只关联最近的描述对象

    • 示例:“多个异常,最大者约 15mm” → 15mm 只修饰"最大者",不修饰所有异常
  8. 优先级规则类:多个规则冲突时的处理顺序

    • 示例:当拆分规则与完整性规则冲突时,优先保证语义完整

六、技术之外的思考

为什么样例描述如此有效?

从认知科学角度,这其实是元认知策略的应用:

  1. 注意引导:描述像聚光灯,指向关键信息
  2. 模式识别:帮助模型建立"如果 - 那么"的产生式规则
  3. 错误预防:提前预警常见陷阱,抑制冲动回答

这和人教书写的"例题 + 解析"模式异曲同工:

  • 只有例题 = 让你自己悟(V3.0)
  • 例题 + 解析 = explicitly 告诉你为什么(V4.0)

对 Prompt Engineering 的启示

传统的 Few-shot Learning 研究过于关注:

  • 样例数量
  • 样例选择策略
  • 样例排序

却忽视了样例的可解释性。我们的实践证明:

样例描述 > 样例数量

这是一个 ROI 极高的改进方向。


七、代码实现

核心代码片段

def build_extraction_prompt(text: str) -> str:
    """生成带样例描述的完整提示词"""
    
    # 1. 基础指令
    base = BASE_INSTRUCTION.strip()
    
    # 2. 构建样例块(关键:包含描述)
    example_blocks = []
    for i, ex in enumerate(FEWSHOT_EXAMPLES, 1):
        block = f"""
【示例 {i}】
示例特点:{ex.get('description', '')}
输入:{ex['input']}
输出:{ex['output']}
"""
        example_blocks.append(block)
    
    examples_str = "\n".join(example_blocks)
    
    # 3. 组装完整提示词
    prompt = f"""{base}

## 关键样例解析
下面是精心设计的示例,请重点关注每个示例的特点说明:

{examples_str}

## 任务
现在请对下方"输入文本"进行同样格式的抽取,只输出 JSON 数组:

输入文本:{text}
"""
    return prompt

八、未来方向

基于这次经验,我正在探索:

  1. 自动化样例描述生成:用更强的模型为现有样例写描述
  2. 动态样例选择:根据输入文本特征,动态匹配最相关的样例
  3. 样例描述模板库:沉淀可复用的描述模式
  4. A/B 测试框架:系统化评估不同描述方式的效果

结语

回顾整个迭代过程,最大的感悟是:

Prompt Engineering 不是玄学,而是精细的认知工程。

每一个看似"简单"的改进(比如加一句描述),背后都是对任务本质的深入理解。

希望我们的经验能帮你少走弯路。如果你在做大模型信息抽取,欢迎交流讨论!


附录:快速上手清单

如果你明天就要开始做类似任务,按这个顺序来:

  • 1. 先写基础字段定义(不用太细,后续会迭代)
  • 2. 收集 10-20 条典型输入文本
  • 3. 人工标注这些文本的正确输出
  • 4. 分析错误模式,归类为 3-5 种类型
  • 5. 为每种错误类型编写 2-3 个针对性样例
  • 6. 给每个样例写描述(最重要!)
  • 7. 测试效果,收集新的错误案例
  • 8. 回到第 4 步,持续迭代

记住:好的提示词不是写出来的,是迭代出来的!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐