
1. 问题背景
在构建一个轻量化 RAG(检索增强生成)系统时,使用了 LangChain 框架结合自托管的 FreeLLMApi(兼容 OpenAI 格式)进行文本向量化。在开发过程中,遇到了运行时类型错误,并对 Embedding 模型如何处理文本以及自托管服务与 OpenAI 原生服务的差异产生了疑问。
2. 运行时问题分析
2.1 错误现象
错误日志:
TypeError: Document.__init__() got an unexpected keyword argument 'page_content'
2.2 根本原因
该错误发生在 load_chunks_from_file 函数中。代码尝试通过 Document(page_content=..., metadata=...) 实例化对象。
- 原因 :实例化时传递的参数名
page_content与当前环境中安装的Document类定义不匹配。这通常是因为导入了错误的Document类,或者使用了版本不兼容的 LangChain 核心库。 - 解决方案 :确保从
langchain_core.documents正确导入Document类,并核对该版本类构造函数的参数名。
3. 深度技术探讨:Embedding 工作机制
3.1 文本是如何转换为向量的?
调用 embeddings.embed_documents(batch_texts) 时,内部执行流程如下:
原始文本 →\rightarrow→ Tokenizer分词 →\rightarrow→ Token IDs (整数序列) →\rightarrow→ 神经网络 →\rightarrow→ Embedding向量
核心结论:神经网络无法直接处理字符串,必须经过 Token 化转换为数字 ID 才能进行矩阵运算。
3.2 _tokenize 方法的作用
在 LangChain 的 OpenAIEmbeddings 实现中,_tokenize 方法的作用不是预处理文本 ,而是计数(量长度)。
- 目的:计算当前文本包含多少个 token,以判断是否超过模型最大长度限制(例如 8191 tokens)。
- 逻辑 :如果超限 →\rightarrow→ 执行截断或切分 →\rightarrow→ 再次检查;如果不超限 →\rightarrow→ 进入
_get_len_safe_embeddings阶段。
3.3 _get_len_safe_embeddings 解析
该方法名为 "Length Safe",意味着它假设输入文本已经通过了长度校验,因此它会跳过所有校验步骤 ,直接将原始字符串(batch_texts)发送给 API 接口。
4. 自托管服务 vs OpenAI 原生 API
4.1 推理逻辑对比
| 特性 | OpenAI 原生 API | 自托管推理服务 (如 FreeLLMApi) |
|---|---|---|
| 传输内容 | 原始字符串 (str) |
可能是字符串,也可能是 Token IDs |
| Tokenize 位置 | 服务端完成 | 可能在客户端 完成 →\rightarrow→ 传输 IDs |
| API 接口 | https://api.openai.com/v1 |
私有 IP (如 172.21.6.101) + 自定义端口 |
| 模型定义 | 明确的模型名 (如 text-embedding-3) |
可能使用 auto 等路由别名 |
4.2 如何判断是否为自建推理服务?
- IP 地址 :使用私有 IP 段(
10.x,172.16-31.x,192.168.x)。 - 端口号 :使用非标准 HTTPS 端口(如
3001)。 - 客户端类型 :
self.client的类型不是标准openai.OpenAI类。 - 输入参数 :API 调用时传递的是
batch_tokens(Listint) 而非batch_texts(Liststr)。
5. 适配方案:让自托管服务走 OpenAI 逻辑
若自托管服务兼容 OpenAI 格式,但其 SDK 强制要求传递 batch_tokens,而你希望传输原始文本,可采取以下方案:
方案 A:重写 Embedding 类 (推荐)
继承 OpenAIEmbeddings 并重写 embed_documents,绕过内部的 Token 转换逻辑,强制发送原始字符串。
方案 B:直接使用 HTTP 请求
不使用 LangChain 的封装,直接通过 requests 或 httpx 调用 /v1/embeddings 端点,确保 input 字段传递的是字符串列表。
python
# 伪代码示例
response = requests.post(
url="http://172.21.6.101:3001/v1/embeddings",
json={"model": "auto", "input": ["原始文本1", "原始文本2"]},
headers={"Authorization": f"Bearer {API_KEY}"}
)
6. 总结
- 运行时错误 →\rightarrow→ 检查类导入与参数匹配。
- Embedding 流程 →\rightarrow→ 本地 Tokenize 仅用于长度校验 →\rightarrow→ 传输原始文本 →\rightarrow→ 服务端 Tokenize →\rightarrow→ 生成向量。
- 自托管识别 →\rightarrow→ 通过私有 IP、非标端口及
self.client类型判断。 - 适配关键 →\rightarrow→ 确认 API 接收的是
string还是token_ids,通过重写方法或直接调用 REST API 解决不兼容问题。