在 NLP 开发中,当内置的命名实体识别(NER)任务无法识别医疗领域的 “药物分子式”,或文本分类需要处理自定义标签时,我们往往需要跳出内置功能的限制。spacy-llm 的自定义任务框架正是为这类场景而生 —— 通过标准化的提示生成与响应解析接口,开发者能快速实现领域特定的 NLP 任务。本文将从框架原理到实战案例,带您掌握自定义 LLM 任务的核心技术。
一、任务开发的核心框架:两大函数与注册表机制
1. 任务开发的 “双引擎”
每个自定义任务都需实现两个核心函数,如同汽车的引擎与变速箱,缺一不可:
① generate_prompts:文档到提示的 “翻译官”
- 输入:spaCy 的 Doc 对象列表(包含分词、句法等预处理信息)
- 输出:LLM 可理解的提示列表
- 核心职责:将结构化文档转化为自然语言指令,可注入领域知识、示例或格式约束
② parse_responses:响应到标注的 “结构化工厂”
- 输入:LLM 返回的自由文本响应列表、原始 Doc 对象
- 输出:标注后的 Doc 对象(如设置 ents、cats 等属性)
- 核心职责:从非结构化文本中提取关键信息,映射到 spaCy 的标准数据结构
2. 注册表机制:让自定义任务 “可插拔”
通过@registry.llm_tasks
装饰器注册自定义任务,只需一行代码即可接入框架:
python
运行
from spacy_llm.registry import registry
@registry.llm_tasks("my_ner.MyMedicalNER.v1")
def create_medical_ner(labels: str):
return MedicalNERTask(labels=labels.split(","))
注册后,即可在配置文件中像内置任务一样引用,实现 “一次开发,多处复用”。
二、实战案例:开发医疗领域自定义 NER 任务
假设我们需要从电子病历中识别 “药物名称”“剂量”“适应症” 三类实体,内置 NER 无法满足需求,需自定义任务。
1. 提示设计:注入领域知识的 “说明书”
关键技巧:
- 标签定义:明确告知 LLM 每个标签的含义(避免歧义)
text
以下是需要识别的实体类型: - 药物名称:具体的药品通用名或商品名(如“阿司匹林”) - 剂量:药品使用的剂量单位及数值(如“500mg”) - 适应症:该药物适用的疾病或症状(如“高血压”)
- 格式约束:要求返回 JSON 数组,便于解析
text
请以JSON数组形式返回识别结果,格式: [{"text": "实体文本", "label": "实体类型"}] 若无匹配实体,返回空数组。
- 少样本示例:通过
few-shot
文件注入典型案例(提升复杂场景准确率)yaml
# ner_examples.yml - text: "患者服用阿司匹林500mg治疗高血压" entities: - text: "阿司匹林" label: "药物名称" - text: "500mg" label: "剂量" - text: "高血压" label: "适应症"
2. 响应解析:从自由文本到 doc.ents 的 “转换器”
核心代码逻辑:
python
运行
from spacy.tokens import Doc, Span
from typing import Iterable, List
class MedicalNERTask:
def __init__(self, labels: List[str]):
self.labels = labels
def generate_prompts(self, docs: Iterable[Doc]) -> Iterable[str]:
prompts = []
for doc in docs:
prompt = f"请从以下文本中识别{', '.join(self.labels)}:\n{doc.text}\n{格式约束}\n{示例引导}"
prompts.append(prompt)
return prompts
def parse_responses(self, docs: Iterable[Doc], responses: Iterable[str]) -> Iterable[Doc]:
for doc, resp in zip(docs, responses):
try:
entities = json.loads(resp) # 解析JSON响应
for ent_info in entities:
# 查找实体在doc中的位置
start = doc.text.find(ent_info["text"])
end = start + len(ent_info["text"])
# 创建Span对象并添加到doc.ents
span = Span(doc, start, end, label=ent_info["label"])
doc.ents = list(doc.ents) + [span]
except JSONDecodeError:
# 处理解析失败,记录警告
logger.warning(f"响应解析失败:{resp}")
return docs
3. 配置文件集成:一键启用自定义任务
ini
[components.llm]
factory = "llm"
[components.llm.task]
@llm_tasks = "my_ner.MyMedicalNER.v1" # 引用注册的任务
labels = "药物名称,剂量,适应症"
[components.llm.task.examples]
@misc = "spacy.FewShotReader.v1" # 加载少样本示例
path = "ner_examples.yml"
[components.llm.model]
@llm_models = "spacy.GPT-3-5.v1"
三、进阶技巧:应对复杂场景的 “工具箱”
1. 提示分片(Prompt Sharding):驯服超长文本
当处理数千字的法律合同或医学文献时,需将文本拆分为模型可处理的片段:
python
运行
def generate_prompts(self, docs: Iterable[Doc]) -> Iterable[List[str]]:
sharded_prompts = []
for doc in docs:
# 按512token分片
chunks = [doc[i:i+512] for i in range(0, len(doc), 512)]
prompts = [f"处理以下文本片段:{chunk.text}" for chunk in docs]
sharded_prompts.append(prompts)
return sharded_prompts
def parse_responses(
self, docs: Iterable[Doc], responses: Iterable[List[str]]
) -> Iterable[Doc]:
# 合并各分片的实体识别结果
for doc, resp_list in zip(docs, responses):
for resp in resp_list:
# 解析并合并实体
pass
return docs
2. 评分函数(Scorer):量化任务表现
自定义实体边界准确率评估:
python
运行
from spacy.training import Example
class MedicalNERTask:
# ...其他代码...
def scorer(self, examples: Iterable[Example]) -> dict:
correct = 0
total = 0
for example in examples:
pred_ents = list(example.predicted.ents)
gold_ents = list(example.reference.ents)
# 计算边界匹配准确率
correct += len(set(pred_ents) & set(gold_ents))
total += len(gold_ents)
return {"boundary_accuracy": correct / total if total else 0.0}
3. 类型验证:避免 “牛头不对马嘴”
通过validate_types=True
(默认开启)检查输入输出一致性:
- 确保
generate_prompts
返回字符串列表,而非其他类型 - 确保
parse_responses
正确设置doc.ents
或doc.cats
ini
[components.llm]
validate_types = true # 开启类型验证
四、最佳实践:避坑指南与效率提升
1. 提示设计的 “三要三不要”
最佳实践 | 反例 | 正例 |
---|---|---|
要格式约束 | 自由文本返回 | 要求 JSON/CSV 格式 |
要标签定义 | 仅列标签名(如 “DRUG”) | 附带标签说明(如 “DRUG:药物名称”) |
要示例引导 | 零样本直接识别 | 提供 2-3 个典型案例 |
不要歧义 | 使用多义词标签(如 “AGE”) | 明确 “AGE:患者年龄” |
不要冗余 | 重复描述任务 | 简洁说明核心需求 |
不要开放结尾 | “请分析文本” | “请返回实体列表,无实体则返回空数组” |
2. 处理未登录标签(OOV Labels)
定义默认响应规则,避免 LLM 编造标签:
text
# 提示末尾添加
若无法识别任何指定标签,请勿编造,请返回空数组。
解析时忽略未定义标签:
python
运行
if ent_info["label"] not in self.labels:
continue # 跳过未注册的标签
3. 性能优化:批量处理与缓存
- 批量提示:使用
nlp.pipe(docs, batch_size=16)
而非单文档处理,降低 API 调用频次 - 结果缓存:通过
save_io=True
存储提示 / 响应,避免重复处理相同文本
python
运行
nlp = assemble("config.cfg")
nlp.get_pipe("llm").config["save_io"] = True # 启用缓存
五、技术亮点:解析 spacy.NER.v3 的链式推理
内置的spacy.NER.v3
采用 Chain-of-Thought(CoT)推理,通过以下步骤提升准确率:
- 分步提示:先要求 LLM 列出可能的实体候选,再分类验证
- 逻辑链条:“首先识别所有可能的实体,然后根据定义判断类型”
- 错误校准:通过少样本示例展示推理过程,引导 LLM 遵循特定逻辑
这种机制在处理嵌套实体或模糊边界时尤为有效,自定义任务可借鉴此思路设计提示模板。
六、总结:打造专属你的 NLP “瑞士军刀”
spacy-llm 的自定义任务框架让领域特定 NLP 开发不再依赖复杂训练:
- 零代码门槛:无需深度学习知识,通过提示工程实现定制功能
- 快速迭代:修改提示模板即可适应需求变化,分钟级验证新方案
- 无缝集成:与 spaCy 原生组件协同,构建混合式智能管道
在实践中,建议从简单场景(如新增标签的文本分类)入手,熟悉提示设计与响应解析逻辑。遇到复杂需求时,善用少样本示例和格式约束提升鲁棒性。当内置任务无法满足时,自定义任务框架就是你的 “破局利器”。
希望这些经验能帮你在 NLP 开发中突破内置功能的限制。如果觉得有用,欢迎点赞收藏,后续我们将分享更多关于模型集成与性能优化的实战技巧!