【AI】从混沌到精准:LLM 结构化信息抽取的提示词迭代之路
声明:本文分享的是通用 Prompt Engineering 方法论,所用示例均为虚构场景,不涉及任何真实业务数据、商业机密或具体项目指标。核心技术思路可应用于医疗、金融、法律、政务等各领域的结构化信息抽取任务。
适用读者:NLP 工程师、大模型应用开发者、Prompt Engineering 实践者
引言
在大模型应用开发中,如何从非结构化文本中高质量地提取结构化数据,一直是个极具挑战的难题。
最近我完成了一个复杂的信息抽取任务,经历了从"一套提示词走天下"到"精细化样例设计"的完整迭代过程。最后一个"不起眼"的细节改进,让提取效果提升了 40%+,模型幻觉降低了 80%。
本文将完整复盘整个提示词优化历程,分享那些真正有效的实战经验。如果你也在做类似的 NLP 任务,相信这些洞察能帮你少走弯路。
一、任务背景(虚构示例)
为了便于说明,本文使用一个虚构的"产品缺陷报告分析"场景:
目标:从用户提交的产品缺陷报告中提取结构化信息
输出字段(9 个核心字段):
- 缺陷类型:功能异常、外观问题、性能不足等
- 发生部位:屏幕、电池、摄像头、按键等
- 问题数量:1 个、多个、多处等
- 缺陷特征:裂纹、变色、卡顿、异响等(核心字段)
- 测量参数:裂纹长度、温度数值等
- 边缘描述:规则、不规则、锯齿状等
- 复现条件:充电时、高温环境下等
- 前后对比:升级后加重、维修后复发等
- 影响范围:导致无法开机、影响正常使用等
核心难点:
- 用户描述口语化,同一概念多种表述(如"充不进电"vs"充电无反应")
- 一段文字包含多个独立缺陷(如"屏幕有划痕,而且充电时会发烫")
- 指代模糊,“这个问题”、"那个地方"频繁出现
- 主观评价与客观描述混杂(如"质量太差了,用了三天就坏了")
说明:虽然是虚构场景,但上述难点在所有领域的信息抽取任务中都具有共性。思路和方案来源于真实项目经历。
二、迭代历程
V1.0:一套提示词打天下
方案特点:
# 单个通用提示词模板
prompt = """
你是一个产品缺陷信息提取助手...
## 字段定义
(9 个字段的详细说明)
## 少样本示例
【示例 1】手机类缺陷案例
【示例 2】电脑类缺陷案例
【示例 3】家电类缺陷案例
...(共 15 个综合样例)
## 任务
请对下方文本进行抽取,只输出 JSON 数组:
输入文本:{text}
"""
结果: 效果不理想,大量错误
典型错误:
输入:"屏幕左上角有划痕,大概 2cm;充电时会发烫,温度超过 40 度"
错误输出:
{
"发生部位": ["屏幕", "充电时"], // ❌ 混为一行,且"充电时"不是部位
"缺陷特征": ["划痕", "发烫"]
}
正确输出应该是两行独立的 JSON 对象:
[
{"发生部位": ["屏幕左上角"], "缺陷特征": ["划痕"], "测量参数": ["大概 2cm"]},
{"发生部位": ["机身"], "缺陷特征": ["发烫"], "复现条件": ["充电时"], "测量参数": ["温度超过 40 度"]}
]
问题分析:
- 样例过于复杂:单个样例包含多种提取模式(部位识别 + 数量统计 + 条件判断)
- 学习目标模糊:模型难以从混杂的样例中归纳出清晰的拆分规则
- 领域差异干扰:不同产品类型的术语差异导致混淆(如手机的"刘海屏"vs 电脑的"全面屏")
V2.0:分品类定制提示词
方案改进:
# 为每个产品品类创建独立提示词
prompts/
├── prompt_手机.py # 12 个手机专用样例
├── prompt_电脑.py # 10 个电脑专用样例
├── prompt_家电.py # 15 个家电专用样例
└── ...
样例特点(以手机为例):
FEWSHOT_EXAMPLES = [
{
"input": """屏幕右上角有裂纹,长度约 3cm,
不影响显示,触摸正常。""",
"output": [{
"缺陷类型": ["外观问题"],
"发生部位": ["屏幕右上角"],
"问题数量": ["1"],
"缺陷特征": ["裂纹"],
"测量参数": ["长度约 3cm"],
"影响范围": ["不影响显示,触摸正常"]
}]
},
# ... 更多手机专属样例
]
结果: 效果明显提升
收益:
- 样例与目标文本分布一致,减少领域偏移
- 术语一致性提高,模型更容易学习规律
- 同一品类的描述习惯相似,降低理解成本
新问题:
- 维护成本激增:10 个品类=10 套提示词,每次修改要同步多处
- 样例膨胀:每个模板都想要覆盖所有边界情况,样例数突破 150+
- 冗余严重:不同品类的样例有大量重复规则(如"看到’大概’要保留到测量参数")
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 度"]},
{"发生部位": ["机身"], "缺陷特征": ["发热"], "复现条件": ["充电时"], "测量参数": ["温度更高"]}
]
}
为什么样例描述如此有效?
- 明确学习重点:直接告诉模型"这个样例你要学什么",避免瞎猜
- 减少歧义:同样的输入输出对,可能有多种解读方式,描述消除了不确定性
- 元学习效应:描述相当于"思维链",教会模型如何思考而不仅仅是模仿
完整提示词结构:
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(推荐做法)
- 给每个样例写描述:用自然语言说明"这个样例要教什么"
- 单一职责原则:一个样例只演示一种规则/陷阱
- 文本切分:将长文本拆分为语义完整的短片段
- 边界案例优先:优先覆盖容易出错的特殊情况
- 持续迭代:根据错误案例不断补充针对性样例
DON’T(避免踩坑)
- 不要堆砌样例:100 个模糊样例不如 20 个精准样例
- 不要让模型猜意图:显式说明规则,不要依赖模型自己总结
- 不要混合多种规则:一个样例同时教拆分 + 指代 + 数量转换=灾难
- 不要忽略阴性样本:明确告诉模型什么情况不提取
- 不要一次性处理长文本:先切分再提取,效果天壤之别
五、样例描述编写技巧
经过实践,我们发现好的样例描述应该具备以下特点:
描述模板
{
"description": "本样例需要注意 [具体知识点],特别是 [易错点],[正确处理方式]"
}
优秀案例
# 好的描述:具体、可操作
"description": "本样例需注意:当出现'最大者'且有明确测量参数时,
必须另起一行单独记录该部位的完整特征,
不要将参数合并到其他缺陷中"
# 差的描述:模糊、笼统
"description": "本样例展示了正确的提取方式"
描述分类
我们总结了 8 类高频描述模式,适用于大多数信息抽取场景:
-
拆分规则类:当某个限定词只修饰部分内容时,需要拆分表述
- 示例:“A 和 B 都增大,以 A 为著” → 拆分为两行("为著"只修饰 A)
-
指代消解类:遇到代词需追溯具体指代对象
- 示例:“其内部”、“该位置”、“上述对象” → 替换为具体名称
-
数量转换类:统一数量词的输出格式
- 示例:“一个/一枚/一处” → “1”;“若干/多个/多处” → 保留原词
-
阴性过滤类:区分正常描述与异常描述
- 示例:“未见明显异常”、“基本正常” → 不提取;“轻微磨损”、“局部变形” → 需提取
-
字段归类类:明确特定描述的字段归属
- 示例:“在…条件下出现” → 归入条件字段;“呈…状态” → 归入特征字段
-
固定术语类:识别专业术语作为整体处理
- 示例:专业名词、固定搭配 → 不拆分,整体提取
-
参数绑定类:测量数值只关联最近的描述对象
- 示例:“多个异常,最大者约 15mm” → 15mm 只修饰"最大者",不修饰所有异常
-
优先级规则类:多个规则冲突时的处理顺序
- 示例:当拆分规则与完整性规则冲突时,优先保证语义完整
六、技术之外的思考
为什么样例描述如此有效?
从认知科学角度,这其实是元认知策略的应用:
- 注意引导:描述像聚光灯,指向关键信息
- 模式识别:帮助模型建立"如果 - 那么"的产生式规则
- 错误预防:提前预警常见陷阱,抑制冲动回答
这和人教书写的"例题 + 解析"模式异曲同工:
- 只有例题 = 让你自己悟(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
八、未来方向
基于这次经验,我正在探索:
- 自动化样例描述生成:用更强的模型为现有样例写描述
- 动态样例选择:根据输入文本特征,动态匹配最相关的样例
- 样例描述模板库:沉淀可复用的描述模式
- A/B 测试框架:系统化评估不同描述方式的效果
结语
回顾整个迭代过程,最大的感悟是:
Prompt Engineering 不是玄学,而是精细的认知工程。
每一个看似"简单"的改进(比如加一句描述),背后都是对任务本质的深入理解。
希望我们的经验能帮你少走弯路。如果你在做大模型信息抽取,欢迎交流讨论!
附录:快速上手清单
如果你明天就要开始做类似任务,按这个顺序来:
- 1. 先写基础字段定义(不用太细,后续会迭代)
- 2. 收集 10-20 条典型输入文本
- 3. 人工标注这些文本的正确输出
- 4. 分析错误模式,归类为 3-5 种类型
- 5. 为每种错误类型编写 2-3 个针对性样例
- 6. 给每个样例写描述(最重要!)
- 7. 测试效果,收集新的错误案例
- 8. 回到第 4 步,持续迭代
记住:好的提示词不是写出来的,是迭代出来的!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)