LangExtract 日语信息抽取实战:UnicodeTokenizer、lx.extract 全流程与字符位置对齐
本篇技术指南基于 LangExtract 仓库中的日语抽取示例文档(docs/examples/japanese_extraction.md),讲解如何从日语这类无空格分隔的语言文本中抽取结构化实体。读完本文,你将掌握:为什么日语必须使用 UnicodeTokenizer 而非默认分词器、lx.extract() 完整流水线的各参数含义、UnicodeTokenizer 的源码级分词机制(grapheme 聚类、CJK 强制碎片化),以及抽取结果如何携带精确的字符位置区间(char_interval)完成来源定位。
问题背景:日语为什么需要专用分词器
LangExtract 的核心能力之一是将 LLM 返回的抽取文本精确对齐回源文档的字符位置(source grounding),从而实现可验证、可可视化的抽取结果。对齐过程依赖分词(tokenization):把文本切分为带位置信息的 token,再在其中定位抽取文本。
默认的 RegexTokenizer(定义见 langextract/core/tokenizer.py)基于正则模式 [^\W\d_]+(英文字母序列)、\d+(数字)和符号组来切分。对英文这类以空格分词的语言非常高效,但日语文本如「東京出身の田中さんはGoogleで働いています。」中,汉字、假名之间没有空格。从源码结构看,若使用默认分词器,CJK 字符序列会按 [^\W\d_]+ 被整段合并成少数大 token(如「東京出身の田中さん」可能成为一个 token),导致"田中"这样的实体无法在 token 层面精确框定,对齐质量会显著下降。
因此文档给出了明确提示:对日语这类无空格语言,应使用 UnicodeTokenizer 以保证基于字符的正确分词与对齐("For non-spaced languages like Japanese, use UnicodeTokenizer to ensure correct character-based segmentation and alignment.")。
完整流程示例:从日语文本中抽取人名、地名、组织
下面是原示例文档中的完整可运行示例。输入是「Mr. Tanaka from Tokyo works at Google.」的日语翻译,抽取目标为 Person(人名)、Location(地名)、Organization(组织)三类命名实体:
import langextract as lx
from langextract.core import tokenizer
# Japanese text with entities (Person, Location, Organization)
# "Mr. Tanaka from Tokyo works at Google."
input_text = "東京出身の田中さんはGoogleで働いています。"
# Define extraction prompt
prompt_description = "Extract named entities including Person, Location, and Organization."
# Define example data (few-shot examples help the model understand the task)
examples = [
lx.data.ExampleData(
text="大阪の山田さんはソニーに入社しました。", # Mr. Yamada from Osaka joined Sony.
extractions=[
lx.data.Extraction(extraction_class="Location", extraction_text="大阪"),
lx.data.Extraction(extraction_class="Person", extraction_text="山田"),
lx.data.Extraction(extraction_class="Organization", extraction_text="ソニー"),
]
)
]
# 1. Initialize the UnicodeTokenizer
# Essential for Japanese to ensure correct grapheme segmentation.
unicode_tokenizer = tokenizer.UnicodeTokenizer()
# 2. Run Extraction with the Custom Tokenizer
result = lx.extract(
text_or_documents=input_text,
prompt_description=prompt_description,
examples=examples,
model_id="gemini-3.5-flash",
tokenizer=unicode_tokenizer, # <--- Pass the tokenizer here
api_key="your-api-key-here" # Optional if env var is set
)
# 3. Display Results
print(f"Input: {input_text}\n")
print("Extracted Entities:")
for entity in result.extractions:
position_info = ""
if entity.char_interval:
start, end = entity.char_interval.start_pos, entity.char_interval.end_pos
position_info = f" (pos: {start}-{end})"
print(f"• {entity.extraction_class}: {entity.extraction_text}{position_info}")
# Expected Output:
# Input: 東京出身の田中さんはGoogleで働いています。
#
# Extracted Entities:
# • Location: 東京 (pos: 0-2)
# • Person: 田中 (pos: 5-7)
# • Organization: Google (pos: 10-16)
示例的关键点逐一说明:
lx.data.ExampleData/lx.data.Extraction:langextract.data是一个向后兼容模块,langextract/data.py 实际是兼容性 shim,把全部符号从 langextract/core/data.py 重新导出。Extraction的核心字段是extraction_class(实体类别)、extraction_text(实体文本),以及用于结果对齐的char_interval(见后文)。few-shot 示例用于让模型理解任务格式,并参与生成输出 schema 约束。model_id="gemini-3.5-flash":model_id会被 provider 路由解析为具体的模型提供者。API key 除了显式传入api_key参数外,也可走环境变量:从 langextract/factory.py 的实现看,Gemini 类模型会依次尝试GEMINI_API_KEY和LANGEXTRACT_API_KEY,GPT 类模型尝试OPENAI_API_KEY和LANGEXTRACT_API_KEY,若同时检测到多个 key 会发出警告并取第一个。tokenizer=unicode_tokenizer:这是本示例的核心改动,将自定义分词器注入抽取管线(机制见下一节)。- 期望输出中的
pos区间:0-2即字符text[0:2]== 「東京」,5-7对应「田中」,10-16对应「Google」。这是"左闭右开"的字符索引约定。
源码解析:UnicodeTokenizer 如何切分日语
UnicodeTokenizer 定义于 langextract/core/tokenizer.py,其类 docstring 明确了三个设计要点:
- 基于 Unicode 标准的 grapheme 聚类:分词循环使用
regex库的\X模式(regex.finditer(r"\X", text),见 tokenizer.py#L352)。\X对应 Unicode 标准附录 #29 定义的"扩展词群"(extended grapheme cluster),能正确把 Emoji、韩文音节等组合字符当作整体处理,而不是按码点机械拆分。 - 不做 NFC 归一化:与某些 Unicode 分词器不同,
UnicodeTokenizer不会把文本归一化为 NFC 形式。TokenizedText的 docstring 特别注明 "For UnicodeTokenizer, this is NOT normalized to NFC (to preserve indices)"。这一设计保证了 token 上的字符索引与原始输入字符串严格一致——这正是char_interval位置信息可靠的前提。 - 性能取舍:由于逐 grapheme 聚类,
UnicodeTokenizer比RegexTokenizer慢;文档注释也指出RegexTokenizer对英文更快,因为跳过了复杂的 Unicode 处理。因此只在处理日/韩/泰等无空格语言时才切换分词器。
CJK 强制碎片化是该分词器对日语最关键的行为。源码中定义了两个模式(tokenizer.py#L249-L254):
_CJK_PATTERN = regex.compile(
r"\p{Is_Han}|\p{Is_Hiragana}|\p{Is_Katakana}|\p{Is_Hangul}"
)
_NON_SPACED_PATTERN = regex.compile(
r"\p{Is_Thai}|\p{Is_Lao}|\p{Is_Khmer}|\p{Is_Myanmar}"
)
在合并逻辑中(tokenizer.py#L370-L395):当当前 token 类型为 WORD 时,若当前或下一个字符命中 _CJK_PATTERN / _NON_SPACED_PATTERN,should_merge 强制为 False,即每个 CJK 字符(汉字、平假名、片假名、韩文)以及泰文、老挝文、高棉文、缅文字符都单独成为一个 token,绝不互相合并。对示例句「東京出身の田中さんは…」,「東京」会被切成「東」「京」两个 token,各自携带精确的字符区间。这样"田中"(第 5-7 字符)就对应连续 token 区间,resolver 可以精确重建出 CharInterval(5, 7)。而 "Google" 这类拉丁字母序列仍按同脚本合并为单个 token(脚本判定有 ASCII 快速路径和按脚本分组逻辑,见 _get_script_fast,tokenizer.py#L273-L279)。
此外,token 类型由 unicodedata.category 判定(L 开头为 WORD,N 开头为 NUMBER,其余为 PUNCTUATION,见 _classify_grapheme),所以句读「。」会作为 PUNCTUATION token 独立切出,而不是黏附在相邻词上。
句界识别也覆盖日语标点:_END_OF_SENTENCE_PATTERN 为 [.?!。!?\u0964](tokenizer.py#L152),即日文句号「。」和感叹/疑问号「!?」都是句子终止符。find_sentence_range()(tokenizer.py#L580-L647)利用这些标点 token 和"换行 + 首字母大写"规则切分句子,为小上下文场景确定句子边界。测试用例 test_unicode_sentence_boundaries 验证了这一点:输入「こんにちは。」被切为 6 个 token——注释明确写道 "こんにちは (5 tokens due to CJK fragmentation) + 。 (1 token) = 6 tokens"(tests/tokenizer_test.py#L969-L975),恰好印证了 CJK 逐字碎片化行为。
tokenizer 参数在 extract() 管线中的位置
extract() 的签名中 tokenizer 是一个显式参数(langextract/extraction.py#L74),docstring 说明其用途:"Optional Tokenizer instance to use for chunking and alignment. If None, defaults to RegexTokenizer."(用于切块(chunking)与对齐(alignment),缺省为 RegexTokenizer,见 extraction.py#L91-L92)。
从源码结构看,分词结果在管线中承担两个职责(与 tokenizer.py 模块 docstring 一致):
- 对齐(alignment):langextract/resolver.py 负责把 LLM 的原始文本输出解析为结构化
Extraction对象,并把每个抽取文本对齐回源文档。对齐算法在 token 序列上运行(精确匹配默认使用 DP 动态规划,模糊匹配使用 LCS,见 resolver.py#L57-L85 中的_FUZZY_ALGORITHM_LCS、_EXACT_ALGORITHM_DP等常量)。分词越贴合目标语言的书写特征,token 级匹配就越准,char_interval也就越精确。 - 句子/上下文切分:
find_sentence_range()基于 token 序列确定句子边界,用于构建较小的上下文片段。
Tokenizer 是抽象基类(tokenizer.py#L165-L177),只需实现 tokenize(text) -> TokenizedText,因此你也可以继承它来自定义分词策略,而 lx.extract() 会接受任何该基类的实例。lx.tokenizer 顶层惰性模块也直接指向 langextract.tokenizer(langextract/init.py#L64-L84),所以示例中使用 from langextract.core import tokenizer 与 lx.tokenizer 是等价路径。
结果结构:Extraction 与 char_interval 的来源定位
抽取结果中每个实体都是一个 Extraction 对象,定义于 langextract/core/data.py。示例中打印 entity.char_interval.start_pos / end_pos 的字段语义如下:
char_interval(CharInterval):源文本中的字符区间,start_pos含边界、end_pos不含(左闭右开)。当抽取文本无法在源文档中定位时,该字段为None,示例代码中if entity.char_interval:的判空正是为此设计。alignment_status(AlignmentStatus):标记对齐方式,取值为MATCH_EXACT(精确匹配)、MATCH_GREATER/MATCH_LESSER(模型输出与源文本长度不等)或MATCH_FUZZY(模糊匹配命中)。- 除示例用到的两个字段外,
Extraction还支持description、attributes(core/data.py#L80-L92)等字段,可用于带属性的抽取任务。
对日语示例而言,位置 0-2 / 5-7 / 10-16 这类区间意味着下游可以做原文高亮、可视化核对(lx.visualize)或基于位置的聚合统计,而不是只拿到一段"模型声称"的文本。
运行前提与适用限制
- 模型与密钥:示例使用
model_id="gemini-3.5-flash",需要可用密钥;如上文所述,api_key参数可省略的前提是对应环境变量(GEMINI_API_KEY/LANGEXTRACT_API_KEY)已设置。 - 何时必须切换分词器:处理日语、韩语,以及泰语、老挝语、高棉语、缅甸语等无空格语言时,建议使用
UnicodeTokenizer;纯英文场景保留默认RegexTokenizer即可(更快)。 - 性能注意:
UnicodeTokenizer的逐 grapheme 聚类开销高于正则分词,类注释中明确 "Grapheme clustering makes this tokenizer slower than RegexTokenizer",大批量文本时这一点会影响本地预处理耗时(不影响 LLM 计费)。 - 验证方式:仓库中 tests/tokenizer_test.py 覆盖了 CJK 字符扩展范围检测、CJK 不合并、Unicode 句界(日文句号)等用例,可作为修改分词策略后的回归参照。
综上,该日语抽取示例展示了 LangExtract 处理无空格语言的完整路径:用 UnicodeTokenizer 的逐字符碎片化分词换取 token 级对齐精度,由 extract() 管线中的 resolver 将 LLM 输出对齐回 char_interval,最终得到带可验证字符位置的实体结果。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00