LangChain项目里用Ollama跑本地Embedding模型,绕过Tokenization报错的实战记录
LangChain实战用Ollama本地模型替代OpenAI Embedding的完整解决方案当开发者尝试在LangChain项目中用本地模型替代OpenAI的Embedding服务时常会遇到各种兼容性问题。最近我在一个知识库项目中就踩到了这样的坑——使用Ollama部署的bge-large-zh-v1.5模型时遭遇了令人困惑的invalid input type错误。经过一番源码级调试终于找到了完美绕过Tokenization问题的方案。1. 问题场景与错误复现在LangChain生态中OpenAIEmbeddings是使用最广泛的文本嵌入工具类。当我们需要切换到本地模型时很自然地会想到利用Ollama提供的OpenAI兼容API。典型的初始化代码如下from langchain_openai import OpenAIEmbeddings embeddings OpenAIEmbeddings( modelbge-large-zh-v1.5, openai_api_basehttp://127.0.0.1:11434/v1/, api_keyollama ) output embeddings.embed_query(你好)执行这段代码会抛出以下错误openai.BadRequestError: Error code: 400 - {error: {message: invalid input type, type: api_error}}这个错误表面看是输入类型不合法但实际上隐藏着LangChain与Ollama API之间的深层兼容性问题。2. 问题根源分析通过深入调试我发现问题出在LangChain的Tokenization处理逻辑上。OpenAIEmbeddings内部的工作流程大致如下输入文本首先经过_tokenize方法处理然后调用_get_len_safe_embeddings方法分块处理最后通过HTTP请求发送到API端点关键问题在于Ollama的API期望直接接收原始文本但OpenAIEmbeddings默认会对文本进行Tokenization处理Tokenization后的数据结构与Ollama API的预期格式不匹配错误发生在请求体构造阶段客户端已经转换了原始文本格式3. 解决方案设计与实现要解决这个问题我们需要创建一个新的Embeddings类继承自OpenAIEmbeddings但绕过Tokenization步骤。以下是完整的实现方案from typing import List, Optional from langchain_community.embeddings import OpenAIEmbeddings class OllamaCompatibleEmbeddings(OpenAIEmbeddings): def _tokenize(self, texts: List[str], chunk_size: int) - tuple: 禁用Tokenization直接返回原始文本和索引 indices list(range(len(texts))) return (range(0, len(texts), chunk_size), texts, indices) def _get_len_safe_embeddings( self, texts: List[str], *, engine: str, chunk_size: Optional[int] None ) - List[List[float]]: 直接传递原始文本跳过Token化步骤 _chunk_size chunk_size or self.chunk_size batched_embeddings: List[List[float]] [] for i in range(0, len(texts), _chunk_size): chunk texts[i: i _chunk_size] response self.client.create( inputchunk, # 关键修改直接使用原始文本列表 modelself.model, **{k: v for k, v in self._invocation_params.items() if k ! model} ) if not isinstance(response, dict): response response.model_dump() batched_embeddings.extend(r[embedding] for r in response[data]) return batched_embeddings async def _aget_len_safe_embeddings( self, texts: List[str], *, engine: str, chunk_size: Optional[int] None ) - List[List[float]]: 异步版本处理逻辑 _chunk_size chunk_size or self.chunk_size batched_embeddings: List[List[float]] [] for i in range(0, len(texts), _chunk_size): chunk texts[i: i _chunk_size] response await self.async_client.create( inputchunk, modelself.model, **{k: v for k, v in self._invocation_params.items() if k ! model} ) if not isinstance(response, dict): response response.model_dump() batched_embeddings.extend(r[embedding] for r in response[data]) return batched_embeddings4. 方案使用与效果验证现在我们可以这样使用改造后的Embeddings类embeddings OllamaCompatibleEmbeddings( modelbge-large-zh-v1.5, openai_api_basehttp://127.0.0.1:11434/v1/, api_keyollama, chunk_size512 ) # 测试查询 output embeddings.embed_query(你好) print(len(output)) # 应该输出嵌入向量的维度这个方案的主要优势在于完全兼容现有LangChain生态可以直接替换原来的OpenAIEmbeddings性能无损跳过了不必要的Tokenization步骤反而提升了效率支持异步操作保留了原版的异步处理能力配置灵活可以自由调整chunk_size等参数5. 深入原理与扩展应用理解这个解决方案需要掌握几个关键点Ollama的OpenAI兼容模式Ollama提供了/v1/embeddings端点其请求格式与OpenAI类似但不完全相同LangChain的Embedding处理流程文本预处理默认会Tokenization分块处理考虑模型上下文长度限制API请求构造结果解析这种解决方案不仅适用于bge-large-zh-v1.5模型理论上可以用于任何通过Ollama部署的文本嵌入模型。在实际项目中我还发现这种方案对于处理长文本特别有效因为它避免了不必要的Tokenization开销。对于需要自定义处理的场景可以进一步重写以下方法def _embed(self, texts: List[str]) - List[List[float]]: 完全自定义的嵌入方法 # 自定义实现...6. 性能优化与生产建议在生产环境使用这个方案时有几个优化点值得注意批量处理尽量使用embed_documents而不是embed_query来处理批量文本chunk_size调优根据模型和硬件调整合适的chunk_size错误处理增加对API响应数据的健壮性检查缓存机制对重复文本可以考虑添加缓存层一个增强版的实现可能包含这些改进from functools import lru_cache class EnhancedOllamaEmbeddings(OllamaCompatibleEmbeddings): lru_cache(maxsize1024) def embed_query(self, text: str) - List[float]: 带缓存的查询嵌入 return super().embed_query(text) def _validate_response(self, response): 验证API响应格式 if not response.get(data): raise ValueError(Invalid response format) return response7. 常见问题与排查技巧在实际集成过程中可能会遇到以下问题问题1API连接超时检查Ollama服务是否正常运行curl http://127.0.0.1:11434确认防火墙设置问题2返回的嵌入维度不一致不同模型的嵌入维度不同需要确认模型规格可以在初始化时添加维度验证class ValidatedOllamaEmbeddings(OllamaCompatibleEmbeddings): def __init__(self, expected_dim: int, **kwargs): super().__init__(**kwargs) self.expected_dim expected_dim def _validate_embedding(self, embedding: List[float]): if len(embedding) ! self.expected_dim: raise ValueError(fExpected dimension {self.expected_dim}, got {len(embedding)})问题3处理速度慢尝试调整chunk_size考虑使用异步接口检查模型是否加载到了GPU8. 替代方案比较除了这里介绍的方案外还有几种可能的替代方法方案优点缺点本文方案无缝集成改动最小需要继承和重写方法直接调用Ollama API完全控制流程失去LangChain生态优势使用LangChain的Ollama类官方支持功能可能受限对于大多数场景本文的方案提供了最佳的平衡点——既保持了LangChain的便利性又解决了兼容性问题。