Files
GraphRAGAgent/docs/langextract_specification.md
plf b02d3378fc GraphRAG Studio — initial commit: multimodal RAG system with KG visualization
Full-stack application for document-to-knowledge-graph pipeline:
- Backend: FastAPI + LangGraph ReAct agent + DeepSeek + MinerU parsing
- Frontend: React 19 + Vite + D3.js + shadcn/ui
- Pipeline: MinerU parsing → LangExtract entity extraction → KG building

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 17:30:04 +08:00

673 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LangExtract Pipeline 规范文档
> 基于 [google/langextract](https://github.com/google/langextract) 源码分析
> 版本基线2026-03-04 main 分支
---
## 目录
- [一、输入规范](#一输入规范)
- [1.1 核心入口函数签名](#11-核心入口函数签名)
- [1.2 支持的输入类型](#12-支持的输入类型)
- [1.3 Document 数据结构](#13-document-数据结构)
- [1.4 CSV Dataset 输入](#14-csv-dataset-输入)
- [1.5 URL 文本下载](#15-url-文本下载)
- [1.6 分块参数配置](#16-分块参数配置)
- [1.7 不支持的输入格式](#17-不支持的输入格式)
- [二、模型接入规范](#二模型接入规范)
- [2.1 模型路由机制](#21-模型路由机制)
- [2.2 Gemini Provider](#22-gemini-provider)
- [2.3 OpenAI Provider](#23-openai-provider)
- [2.4 Ollama Provider](#24-ollama-provider)
- [2.5 OpenAI 兼容接口适配DeepSeek 等)](#25-openai-兼容接口适配deepseek-等)
- [2.6 模型优先级与配置覆盖关系](#26-模型优先级与配置覆盖关系)
- [2.7 关于 Embedding 模型](#27-关于-embedding-模型)
- [三、输出数据格式规范](#三输出数据格式规范)
- [3.1 AnnotatedDocument 结构](#31-annotateddocument-结构)
- [3.2 Extraction 结构](#32-extraction-结构)
- [3.3 CharInterval 字符锚点](#33-charinterval-字符锚点)
- [3.4 AlignmentStatus 对齐状态枚举](#34-alignmentstatus-对齐状态枚举)
- [3.5 Resolver 对齐参数](#35-resolver-对齐参数)
- [3.6 JSONL 输出文件格式](#36-jsonl-输出文件格式)
- [3.7 完整输出 JSON Schema 示例](#37-完整输出-json-schema-示例)
- [3.8 HTML 可视化输出](#38-html-可视化输出)
- [附录:环境变量与常量速查](#附录环境变量与常量速查)
---
## 一、输入规范
### 1.1 核心入口函数签名
文件路径:`langextract/extraction.py`
```python
def extract(
text_or_documents: typing.Any,
prompt_description: str | None = None,
examples: typing.Sequence[typing.Any] | None = None,
model_id: str = "gemini-2.5-flash",
api_key: str | None = None,
language_model_type: typing.Type[typing.Any] | None = None, # 已废弃
format_type: typing.Any = None,
max_char_buffer: int = 1000,
temperature: float | None = None,
fence_output: bool | None = None,
use_schema_constraints: bool = True,
batch_length: int = 10,
max_workers: int = 10,
additional_context: str | None = None,
resolver_params: dict | None = None,
language_model_params: dict | None = None,
debug: bool = False,
model_url: str | None = None,
extraction_passes: int = 1,
context_window_chars: int | None = None,
config: typing.Any = None,
model: typing.Any = None,
*,
fetch_urls: bool = True,
prompt_validation_level: PromptValidationLevel = PromptValidationLevel.WARNING,
prompt_validation_strict: bool = False,
show_progress: bool = True,
tokenizer: Tokenizer | None = None,
) -> list[AnnotatedDocument] | AnnotatedDocument
```
**关键参数说明:**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `text_or_documents` | `Any` | **必填** | 纯文本字符串、URL、或 `Document` 对象的可迭代集合 |
| `prompt_description` | `str \| None` | `None` | 抽取提示词,描述需要抽取什么实体 |
| `examples` | `Sequence[Any] \| None` | `None` | **必填** — Few-shot 示例列表(为空则抛出 ValueError |
| `model_id` | `str` | `"gemini-2.5-flash"` | 模型标识符,用于自动路由到对应 Provider |
| `api_key` | `str \| None` | `None` | LLM API Key也可通过环境变量设置 |
| `max_char_buffer` | `int` | `1000` | 每个文本分块的最大字符数 |
| `temperature` | `float \| None` | `None` | 采样温度(`None` 使用模型默认值) |
| `use_schema_constraints` | `bool` | `True` | 是否启用结构化输出约束 |
| `batch_length` | `int` | `10` | 每批处理的文本分块数量 |
| `max_workers` | `int` | `10` | 最大并行工作线程数 |
| `additional_context` | `str \| None` | `None` | 附加到推理提示词中的上下文信息 |
| `resolver_params` | `dict \| None` | `None` | 对齐解析器参数(见 [3.5 节](#35-resolver-对齐参数) |
| `extraction_passes` | `int` | `1` | 抽取轮次(>1 时多次抽取并合并非重叠结果) |
| `context_window_chars` | `int \| None` | `None` | 前一分块的上下文窗口字符数(用于指代消解) |
| `model_url` | `str \| None` | `None` | 自托管模型的 API 端点 URL |
| `fetch_urls` | `bool` | `True` | 是否自动下载 http(s) URL 内容 |
---
### 1.2 支持的输入类型
LangExtract **仅接受纯文本**作为输入,支持以下 4 种传入方式:
| 输入方式 | 示例 | 说明 |
|---------|------|------|
| **纯文本字符串** | `extract("这是一段文本...")` | 直接传入文本内容 |
| **URL** | `extract("https://example.com/article.txt")` | 自动下载 URL 文本内容(`fetch_urls=True` |
| **Document 对象** | `extract([Document(text="...", document_id="doc1")])` | 传入 Document 可迭代集合 |
| **CSV 文件** | 通过 `Dataset` 类加载后传入 | 指定 text 列和 id 列 |
---
### 1.3 Document 数据结构
文件路径:`langextract/core/data.py`
```python
@dataclasses.dataclass
class Document:
text: str # 必填 — 原始文本内容
additional_context: str | None = None # 可选 — 附加上下文
document_id: str # 自动生成 — 格式 "doc_{uuid_hex[:8]}"
tokenized_text: TokenizedText # 惰性计算 — 分词后的文本
```
**字段说明:**
- `text`**必填**,原始文本内容,类型为 `str`
- `additional_context`:可选,会附加到推理提示词中
- `document_id`:通过 property 访问,未设置时自动生成格式为 `doc_{uuid_hex[:8]}` 的唯一 ID
- `tokenized_text`:通过 property 惰性计算,使用配置的 Tokenizer 进行分词
---
### 1.4 CSV Dataset 输入
文件路径:`langextract/io.py`
```python
@dataclasses.dataclass(frozen=True)
class Dataset:
input_path: pathlib.Path # CSV 文件路径
id_key: str # 文档 ID 对应的列名
text_key: str # 文本内容对应的列名
def load(self, delimiter: str = ',') -> Iterator[Document]:
"""仅支持 .csv 后缀文件,其他格式抛出 NotImplementedError"""
```
**CSV 文件要求:**
- 文件后缀必须为 `.csv`
- 必须包含 `text_key` 指定的文本列和 `id_key` 指定的 ID 列
- 默认分隔符为逗号(`,`),可通过 `delimiter` 参数修改
- 其他文件格式会直接抛出 `NotImplementedError`
---
### 1.5 URL 文本下载
文件路径:`langextract/io.py`
```python
def download_text_from_url(
url: str,
timeout: int = 30, # 默认超时 30 秒
show_progress: bool = True,
chunk_size: int = 8192,
) -> str
```
**URL 要求:**
- 必须以 `http://``https://` 开头
- 仅下载文本内容(`response.text`),不解析 HTML/PDF 等
- 需要 `fetch_urls=True`(默认开启)
---
### 1.6 分块参数配置
文件路径:`langextract/chunking.py`
LangExtract 使用基于**句子边界**的确定性分块策略(非语义分块),核心类为 `ChunkIterator`
```python
class ChunkIterator:
def __init__(
self,
text: str | TokenizedText | None,
max_char_buffer: int, # 每个分块最大字符数
tokenizer_impl: Tokenizer, # 分词器实例
document: Document | None = None,
)
```
**分块策略:**
1. 如果单个句子超过 `max_char_buffer`,按换行符拆分,同时尊重 token 边界
2. 如果单个 token 超过 `max_char_buffer`,该 token 独占一个分块
3. 如果多个句子可以放入 `max_char_buffer`,合并为一个分块
**TextChunk 输出结构:**
```python
@dataclasses.dataclass
class TextChunk:
token_interval: TokenInterval # 在源文档中的 token 区间
document: Document | None = None # 源文档引用
# 属性
chunk_text: str # 重建的文本内容
sanitized_chunk_text: str # 标准化空白的文本
char_interval: CharInterval # 在源文档中的字符区间
document_id: str | None # 源文档 ID
```
---
### 1.7 不支持的输入格式
以下格式 **不被支持**,需要在 LangExtract 之前通过外部工具预处理为纯文本:
| 格式 | 状态 | 预处理方案 |
|------|------|-----------|
| PDF | ❌ 不支持 | 使用 MinerU / PyMuPDF 先转文本 |
| DOCX | ❌ 不支持 | 使用 python-docx 先转文本 |
| HTML | ❌ 不支持 | 使用 BeautifulSoup 先提取文本 |
| 图片 | ❌ 不支持 | 使用 OCR 工具先识别文本 |
| Markdown含媒体 | ❌ 不支持 | 需提取纯文本部分 |
| Excel / JSON | ❌ 不支持 | 需序列化为纯文本 |
---
## 二、模型接入规范
### 2.1 模型路由机制
文件路径:`langextract/providers/patterns.py`
LangExtract 通过 **正则匹配 `model_id`** 自动路由到对应的 Provider
| Provider | 匹配模式 | 优先级 | 示例模型 |
|----------|---------|--------|---------|
| **Gemini** | `^gemini` | 10 | `gemini-2.5-flash`, `gemini-1.5-pro` |
| **OpenAI** | `^gpt-4`, `^gpt4.`, `^gpt-5`, `^gpt5.` | 10 | `gpt-4o`, `gpt-4o-mini` |
| **Ollama** | `gemma`, `llama`, `mistral`, `phi`, `qwen`, `deepseek` 等 | 10 | `gemma2:2b`, `llama3.2:1b` |
Ollama 额外支持 HuggingFace 格式的模型名:`meta-llama/Llama*`, `google/gemma*`, `mistralai/*`, `microsoft/phi*` 等。
---
### 2.2 Gemini Provider
文件路径:`langextract/providers/gemini.py`
```python
class GeminiLanguageModel(BaseLanguageModel):
def __init__(
self,
model_id: str = 'gemini-2.5-flash',
api_key: str | None = None,
vertexai: bool = False,
credentials: Any | None = None,
project: str | None = None,
location: str | None = None,
http_options: Any | None = None,
gemini_schema: GeminiSchema | None = None,
format_type: FormatType = FormatType.JSON,
temperature: float = 0.0,
max_workers: int = 10,
fence_output: bool = False,
**kwargs,
)
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `model_id` | `gemini-2.5-flash` | Gemini 模型标识 |
| `api_key` | `None` | 环境变量:`GEMINI_API_KEY``LANGEXTRACT_API_KEY` |
| `vertexai` | `False` | 是否使用 Vertex AI 企业认证 |
| `temperature` | `0.0` | 采样温度(确定性输出) |
| `format_type` | `JSON` | 输出格式 |
**运行时可配参数:** `temperature`, `max_output_tokens`, `top_p`, `top_k`
**额外参数白名单:** `response_schema`, `response_mime_type`, `safety_settings`, `system_instruction`, `tools`, `stop_sequences`, `candidate_count`
---
### 2.3 OpenAI Provider
文件路径:`langextract/providers/openai.py`
```python
class OpenAILanguageModel(BaseLanguageModel):
def __init__(
self,
model_id: str = 'gpt-4o-mini',
api_key: str | None = None,
base_url: str | None = None,
organization: str | None = None,
format_type: FormatType = FormatType.JSON,
temperature: float | None = None,
max_workers: int = 10,
**kwargs,
)
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `model_id` | `gpt-4o-mini` | OpenAI 模型标识 |
| `api_key` | `None` | 环境变量:`OPENAI_API_KEY``LANGEXTRACT_API_KEY` |
| `base_url` | `None` | 自定义 API 端点(用于兼容接口) |
| `organization` | `None` | OpenAI 组织 ID |
| `temperature` | `None` | 采样温度 |
**运行时可配参数:** `temperature`, `max_output_tokens`, `top_p`, `frequency_penalty`, `presence_penalty`, `seed`, `stop`, `logprobs`, `top_logprobs`, `reasoning_effort`, `reasoning`, `response_format`
---
### 2.4 Ollama Provider
文件路径:`langextract/providers/ollama.py`
```python
class OllamaLanguageModel(BaseLanguageModel):
def __init__(
self,
model_id: str, # 必填
model_url: str = 'http://localhost:11434',
base_url: str | None = None,
format_type: FormatType | None = None,
constraint: Constraint = Constraint(),
timeout: int | None = None,
**kwargs,
)
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `model_id` | **必填** | Ollama 模型名(如 `gemma2:2b` |
| `model_url` | `http://localhost:11434` | Ollama 服务地址 |
| `timeout` | `120` | 请求超时(秒) |
| `format_type` | `JSON` | 输出格式 |
**内部默认常量:**
| 常量 | 值 | 说明 |
|------|-----|------|
| `_DEFAULT_TEMPERATURE` | `0.1` | 默认温度 |
| `_DEFAULT_TIMEOUT` | `120` | 默认超时(秒) |
| `_DEFAULT_KEEP_ALIVE` | `300` | 模型保活时间(秒) |
| `_DEFAULT_NUM_CTX` | `2048` | 默认上下文窗口大小 |
**认证支持:** 可配置 `api_key``auth_scheme`(默认 `Bearer`)、`auth_header`(默认 `Authorization`)用于代理 Ollama 实例。
---
### 2.5 OpenAI 兼容接口适配DeepSeek 等)
LangExtract 的 OpenAI Provider 支持 `base_url` 参数,因此可以接入任何 OpenAI 兼容 API
```python
# DeepSeek 接入示例
result = lx.extract(
text_or_documents="...",
model_id="gpt-4o-mini", # 触发 OpenAI Provider 路由
api_key="sk-your-deepseek-key",
examples=[...],
language_model_params={
"base_url": "https://api.deepseek.com",
},
)
```
> **注意:** 由于路由基于 `model_id` 正则匹配,使用 DeepSeek 等兼容接口时 `model_id` 仍需使用 `gpt-*` 前缀来命中 OpenAI Provider或通过 `config` 参数显式指定 Provider。
---
### 2.6 模型优先级与配置覆盖关系
模型配置的优先级从高到低:
```
model预配置的模型实例 > configModelConfig 实例) > model_id + api_key
```
**ModelConfig 结构**`langextract/factory.py`
```python
@dataclasses.dataclass(slots=True, frozen=True)
class ModelConfig:
model_id: str | None = None # 模型标识
provider: str | None = None # 显式指定 Provider 名称
provider_kwargs: dict[str, Any] = field(default_factory=dict) # Provider 构造参数
```
---
### 2.7 关于 Embedding 模型
**LangExtract 不使用也不依赖任何 Embedding 模型。**
- 文本分块使用基于句子边界的确定性分割算法,不涉及语义相似度计算
- 没有向量索引或向量检索功能
- 整个代码库中没有任何 Embedding 相关的调用
---
## 三、输出数据格式规范
### 3.1 AnnotatedDocument 结构
文件路径:`langextract/core/data.py`
```python
@dataclasses.dataclass
class AnnotatedDocument:
extractions: list[Extraction] | None = None # 抽取结果列表
text: str | None = None # 原始文本
document_id: str # 文档唯一标识(自动生成)
tokenized_text: TokenizedText # 分词后文本(惰性计算)
```
**序列化后的 JSON 顶层字段:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `document_id` | `string` | 文档唯一标识,格式 `doc_{uuid_hex[:8]}` |
| `text` | `string \| null` | 原始输入文本 |
| `extractions` | `array[Extraction] \| null` | 抽取的实体列表 |
---
### 3.2 Extraction 结构
文件路径:`langextract/core/data.py`
```python
@dataclasses.dataclass(init=False)
class Extraction:
extraction_class: str # 实体类型
extraction_text: str # 实体文本
char_interval: CharInterval | None = None # 字符位置锚点
alignment_status: AlignmentStatus | None = None # 对齐状态
extraction_index: int | None = None # 抽取顺序索引
group_index: int | None = None # 分组索引
description: str | None = None # 实体描述
attributes: dict[str, str | list[str]] | None = None # 附加属性
token_interval: TokenInterval | None = None # Token 位置锚点
```
**字段详细说明:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `extraction_class` | `str` | 是 | 实体类型/分类名称(如 `PERSON`, `ORGANIZATION` |
| `extraction_text` | `str` | 是 | 抽取的文本内容(应为原文的子串) |
| `char_interval` | `CharInterval \| null` | 否 | 在原文中的字符偏移位置 |
| `alignment_status` | `string \| null` | 否 | 文本对齐质量(见 [3.4 节](#34-alignmentstatus-对齐状态枚举) |
| `extraction_index` | `int \| null` | 否 | 在结果列表中的顺序位置 |
| `group_index` | `int \| null` | 否 | 分组归属(用于关联抽取) |
| `description` | `string \| null` | 否 | 对该实体的补充描述 |
| `attributes` | `dict \| null` | 否 | 键值对形式的附加属性 |
| `token_interval` | `TokenInterval \| null` | 否 | 在原文中的 token 偏移位置 |
---
### 3.3 CharInterval 字符锚点
文件路径:`langextract/core/data.py`
```python
@dataclasses.dataclass
class CharInterval:
start_pos: int | None = None # 起始位置包含0-indexed
end_pos: int | None = None # 结束位置(不包含)
```
**语义:** `source_text[start_pos:end_pos]` 即为抽取的文本在原文中的精确位置。
---
### 3.4 AlignmentStatus 对齐状态枚举
文件路径:`langextract/core/data.py`
```python
class AlignmentStatus(enum.Enum):
MATCH_EXACT = "match_exact"
MATCH_GREATER = "match_greater"
MATCH_LESSER = "match_lesser"
MATCH_FUZZY = "match_fuzzy"
```
| 状态值 | 序列化值 | 含义 | 可信度 |
|--------|---------|------|--------|
| `MATCH_EXACT` | `"match_exact"` | LLM 输出与原文 token 序列完全匹配 | 最高 |
| `MATCH_GREATER` | `"match_greater"` | LLM 输出的 token 序列短于匹配到的原文(找到最佳重叠) | 高 |
| `MATCH_LESSER` | `"match_lesser"` | LLM 输出长于匹配到的原文(部分精确匹配) | 中 |
| `MATCH_FUZZY` | `"match_fuzzy"` | 模糊匹配,重叠率达到阈值(默认 ≥0.75 | 低 |
| `None` | `null` | 未找到任何对齐 | 不可信 |
**对齐流程:**
```
1. 尝试精确 token 级别匹配difflib
├── 成功且长度相等 → MATCH_EXACT
├── 成功但 LLM 输出更长 → MATCH_LESSER
└── 成功但匹配区域更大 → MATCH_GREATER
2. 精确匹配失败且 enable_fuzzy_alignment=True
├── 最佳重叠窗口 ≥ fuzzy_alignment_threshold → MATCH_FUZZY
└── 低于阈值 → None
3. 精确匹配失败且 enable_fuzzy_alignment=False → None
```
---
### 3.5 Resolver 对齐参数
文件路径:`langextract/resolver.py`
通过 `extract()``resolver_params` 字典传入:
```python
result = lx.extract(
...,
resolver_params={
"enable_fuzzy_alignment": True, # 是否启用模糊对齐(默认 True
"fuzzy_alignment_threshold": 0.75, # 模糊匹配最低重叠率(默认 0.75
"accept_match_lesser": True, # 是否接受 MATCH_LESSER默认 True
"suppress_parse_errors": False, # 是否忽略 JSON 解析错误(默认 False
},
)
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `enable_fuzzy_alignment` | `bool` | `True` | 精确匹配失败后是否尝试模糊匹配 |
| `fuzzy_alignment_threshold` | `float` | `0.75` | 模糊匹配的最低 token 重叠比率0.0~1.0 |
| `accept_match_lesser` | `bool` | `True` | 是否接受部分精确匹配结果 |
| `suppress_parse_errors` | `bool` | `False` | JSON 解析失败时是否继续而非报错 |
---
### 3.6 JSONL 输出文件格式
文件路径:`langextract/io.py`
```python
def save_annotated_documents(
annotated_documents: Iterator[AnnotatedDocument],
output_dir: pathlib.Path | str | None = None,
output_name: str = 'data.jsonl',
show_progress: bool = True,
) -> None
```
**输出规范:**
- 文件格式:**JSONL**JSON Lines每行一个完整的 JSON 对象
- 默认文件名:`data.jsonl`
- 序列化规则:
- Enum 值转为字符串(如 `AlignmentStatus.MATCH_EXACT``"match_exact"`
- NumPy / integral 数值类型转为 `int`
-`_` 开头的私有字段被排除
---
### 3.7 完整输出 JSON Schema 示例
单条 JSONL 记录的完整结构:
```json
{
"document_id": "doc_a1b2c3d4",
"text": "GraphRAG is a technique developed by Microsoft Research that combines knowledge graphs with retrieval-augmented generation.",
"extractions": [
{
"extraction_class": "TECHNOLOGY",
"extraction_text": "GraphRAG",
"char_interval": {
"start_pos": 0,
"end_pos": 8
},
"alignment_status": "match_exact",
"extraction_index": 0,
"group_index": null,
"description": "A technique combining knowledge graphs with RAG",
"attributes": {
"category": "AI/ML",
"developer": "Microsoft Research"
},
"token_interval": {
"start_index": 0,
"end_index": 1
}
},
{
"extraction_class": "ORGANIZATION",
"extraction_text": "Microsoft Research",
"char_interval": {
"start_pos": 46,
"end_pos": 64
},
"alignment_status": "match_exact",
"extraction_index": 1,
"group_index": null,
"description": null,
"attributes": null,
"token_interval": {
"start_index": 7,
"end_index": 9
}
}
]
}
```
---
### 3.8 HTML 可视化输出
文件路径:`langextract/visualization.py`
```python
def visualize(doc: AnnotatedDocument) -> HTML
```
**功能特性:**
-`extraction_class` 进行颜色编码高亮10 色调色板)
- 交互式 tooltip 显示实体类型和属性
- 动画导航控件,支持多实体浏览
- 进度滑块
- 响应式 HTML/CSS/JavaScript 嵌入
- 支持 Jupyter / IPython 环境直接渲染
---
## 附录:环境变量与常量速查
### 环境变量
| 变量名 | 适用 Provider | 说明 |
|--------|--------------|------|
| `LANGEXTRACT_API_KEY` | 所有 | 通用 API Key 后备 |
| `GEMINI_API_KEY` | Gemini | Gemini API Key |
| `OPENAI_API_KEY` | OpenAI | OpenAI API Key |
| `OLLAMA_BASE_URL` | Ollama | Ollama 服务地址(默认 `http://localhost:11434` |
### FormatType 枚举
```python
class FormatType(enum.Enum):
YAML = 'yaml'
JSON = 'json'
```
### 结构化输出支持
| Provider | Schema 类型 | 结构化输出模式 |
|----------|------------|---------------|
| Gemini | `GeminiSchema` | 严格结构化输出 |
| OpenAI | JSON Mode | 通过 `response_format` 约束 |
| Ollama | `FormatModeSchema` | JSON 模式(非严格) |
### Fence Output 逻辑
| Provider | 默认值 | 说明 |
|----------|--------|------|
| Gemini | `False` | 有 Schema 时不需要 fence |
| OpenAI | `False` | JSON Mode 返回原始 JSON |
| Ollama | `False` | 返回原始 JSON |