首页
/ LangExtract 日语信息抽取实战:UnicodeTokenizer、lx.extract 全流程与字符位置对齐

LangExtract 日语信息抽取实战:UnicodeTokenizer、lx.extract 全流程与字符位置对齐

2026-09-05 11:04:25作者:傅爽业Veleda

本篇技术指南基于 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.Extractionlangextract.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_KEYLANGEXTRACT_API_KEY,GPT 类模型尝试 OPENAI_API_KEYLANGEXTRACT_API_KEY,若同时检测到多个 key 会发出警告并取第一个。
  • tokenizer=unicode_tokenizer:这是本示例的核心改动,将自定义分词器注入抽取管线(机制见下一节)。
  • 期望输出中的 pos 区间0-2 即字符 text[0:2] == 「東京」,5-7 对应「田中」,10-16 对应「Google」。这是"左闭右开"的字符索引约定。

源码解析:UnicodeTokenizer 如何切分日语

UnicodeTokenizer 定义于 langextract/core/tokenizer.py,其类 docstring 明确了三个设计要点:

  1. 基于 Unicode 标准的 grapheme 聚类:分词循环使用 regex 库的 \X 模式(regex.finditer(r"\X", text),见 tokenizer.py#L352)。\X 对应 Unicode 标准附录 #29 定义的"扩展词群"(extended grapheme cluster),能正确把 Emoji、韩文音节等组合字符当作整体处理,而不是按码点机械拆分。
  2. 不做 NFC 归一化:与某些 Unicode 分词器不同,UnicodeTokenizer 不会把文本归一化为 NFC 形式。TokenizedText 的 docstring 特别注明 "For UnicodeTokenizer, this is NOT normalized to NFC (to preserve indices)"。这一设计保证了 token 上的字符索引与原始输入字符串严格一致——这正是 char_interval 位置信息可靠的前提。
  3. 性能取舍:由于逐 grapheme 聚类,UnicodeTokenizerRegexTokenizer 慢;文档注释也指出 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_PATTERNshould_merge 强制为 False,即每个 CJK 字符(汉字、平假名、片假名、韩文)以及泰文、老挝文、高棉文、缅文字符都单独成为一个 token,绝不互相合并。对示例句「東京出身の田中さんは…」,「東京」会被切成「東」「京」两个 token,各自携带精确的字符区间。这样"田中"(第 5-7 字符)就对应连续 token 区间,resolver 可以精确重建出 CharInterval(5, 7)。而 "Google" 这类拉丁字母序列仍按同脚本合并为单个 token(脚本判定有 ASCII 快速路径和按脚本分组逻辑,见 _get_script_fasttokenizer.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 一致):

  1. 对齐(alignment)langextract/resolver.py 负责把 LLM 的原始文本输出解析为结构化 Extraction 对象,并把每个抽取文本对齐回源文档。对齐算法在 token 序列上运行(精确匹配默认使用 DP 动态规划,模糊匹配使用 LCS,见 resolver.py#L57-L85 中的 _FUZZY_ALGORITHM_LCS_EXACT_ALGORITHM_DP 等常量)。分词越贴合目标语言的书写特征,token 级匹配就越准,char_interval 也就越精确。
  2. 句子/上下文切分find_sentence_range() 基于 token 序列确定句子边界,用于构建较小的上下文片段。

Tokenizer 是抽象基类(tokenizer.py#L165-L177),只需实现 tokenize(text) -> TokenizedText,因此你也可以继承它来自定义分词策略,而 lx.extract() 会接受任何该基类的实例。lx.tokenizer 顶层惰性模块也直接指向 langextract.tokenizerlangextract/init.py#L64-L84),所以示例中使用 from langextract.core import tokenizerlx.tokenizer 是等价路径。

结果结构:Extraction 与 char_interval 的来源定位

抽取结果中每个实体都是一个 Extraction 对象,定义于 langextract/core/data.py。示例中打印 entity.char_interval.start_pos / end_pos 的字段语义如下:

  • char_intervalCharInterval):源文本中的字符区间,start_pos 含边界、end_pos 不含(左闭右开)。当抽取文本无法在源文档中定位时,该字段为 None,示例代码中 if entity.char_interval: 的判空正是为此设计。
  • alignment_statusAlignmentStatus):标记对齐方式,取值为 MATCH_EXACT(精确匹配)、MATCH_GREATER / MATCH_LESSER(模型输出与源文本长度不等)或 MATCH_FUZZY(模糊匹配命中)。
  • 除示例用到的两个字段外,Extraction 还支持 descriptionattributescore/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,最终得到带可验证字符位置的实体结果。

登录后查看全文
热门项目推荐
相关项目推荐