1. 从一次 401 报错说起:LangChain 示例选择器到底在选什么
如果你正在用 LangChain 做 Few-Shot 提示,大概率会遇到这样一个场景:示例库从 5 条涨到 50 条之后,提示词越来越长,token 成本肉眼可见地上升,模型回答反而开始跑偏。这时候就该请出示例选择器(Example Selector)了。它的作用很直白——针对每一次新输入,从示例库里动态挑出最相关的 3 到 5 条,而不是把全部示例一股脑塞进提示词。
但真正动手接的时候,问题往往不在选择器本身,而在鉴权链路上。我见过太多人卡在同一个地方:代码里SemanticSimilarityExampleSelector写得没问题,FewShotPromptTemplate也拼好了,一跑就报401 Unauthorized,或者本地代理直接local proxy failed。原因通常是嵌入模型和对话模型走了两套不同的 Key,或者auth.json里的 endpoint 还指向旧的地址。
这篇就围绕这条排查路径展开:先讲清楚四种示例选择器的选样逻辑和适用边界,再把手把手把 endpoint 和auth.json统一改到 TaoToken 的 API 通道上,最后用一次真实请求验证选样结果和鉴权是否都正常。适合正在学 LangChain、被 401 和代理报错卡住的同学。
核心检索词先摆出来:LangChain 示例选择器是什么、能做什么、适合谁。它是BaseExampleSelector的子类,实现select_examples(input_variables)方法,注入到FewShotPromptTemplate或FewShotChatMessagePromptTemplate里替代固定的examples列表。适合示例库超过 20 条、对 token 成本敏感、或者需要按语义匹配示例的开发者。
2. 四种示例选择器的选样逻辑与 401 报错排查
2.1 LengthBasedExampleSelector:按 token 预算裁剪
这个选择器不关心内容相关性,只关心 token 预算。它按顺序往提示里加示例,直到格式化后的总长度接近max_length就停。参数就三个关键项:examples是示例列表,example_prompt是单条示例的格式化模板,max_length是总 token 上限。
from langchain_core.example_selectors import LengthBasedExampleSelector from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate examples = [ {"input": "happy", "output": "sad"}, {"input": "tall", "output": "short"}, {"input": "sunny", "output": "gloomy"}, {"input": "windy", "output": "calm"}, ] example_prompt = PromptTemplate( input_variables=["input", "output"], template="Input: {input}\nOutput: {output}", ) example_selector = LengthBasedExampleSelector( examples=examples, example_prompt=example_prompt, max_length=25, ) dynamic_prompt = FewShotPromptTemplate( example_selector=example_selector, example_prompt=example_prompt, prefix="给出每个输入的反义词", suffix="Input: {adjective}\nOutput:", input_variables=["adjective"], ) print(dynamic_prompt.invoke({"adjective": "big"}).to_messages()[0].content)它的优点是计算极快、无外部依赖、token 预算绝对不超限;缺点是完全不看相关性,可能选出一堆和当前输入无关的短示例。适合上下文窗口很小、或者示例质量相近只受长度约束的场景。
2.2 SemanticSimilarityExampleSelector:语义向量匹配
这是最常用的一种。它把每条示例的输入通过嵌入模型转成向量存进向量库,新查询也转成向量,然后按余弦相似度找最相似的 k 条。from_examples()是主要入口,参数包括examples、embeddings、vectorstore_cls和k(默认 4)。
import os from langchain_chroma import Chroma from langchain_core.example_selectors import SemanticSimilarityExampleSelector from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate from langchain_community.embeddings import DashScopeEmbeddings examples = [ {"input": "happy", "output": "sad"}, {"input": "tall", "output": "short"}, {"input": "energetic", "output": "lethargic"}, {"input": "sunny", "output": "gloomy"}, {"input": "windy", "output": "calm"}, ] example_prompt = PromptTemplate( input_variables=["input", "output"], template="Input: {input}\nOutput: {output}\n", ) embeddings = DashScopeEmbeddings( model="text-embedding-v3", dashscope_api_key=os.getenv("DASHSCOPE_API_KEY"), ) example_selector = SemanticSimilarityExampleSelector.from_examples( examples, embeddings, Chroma, k=1, ) similar_prompt = FewShotPromptTemplate( example_selector=example_selector, example_prompt=example_prompt, prefix="给出每个输入的反义词", suffix="Input: {adjective}\nOutput:", input_variables=["adjective"], ) print(similar_prompt.invoke({"adjective": "cold"}).to_messages()[0].content)这里就是 401 报错的高发区。DashScopeEmbeddings走的是 DashScope 的 Key,而对话模型可能走的是另一套 Key。两套 Key 各自独立,只要有一边没配好,或者 endpoint 写错,就会在嵌入阶段直接抛 401。更隐蔽的是本地代理场景:环境变量里残留了HTTP_PROXY,请求先打到本地代理端口,代理没起来就报local proxy failed,代理起来了但转发目标不对就报 401。
2.3 MaxMarginalRelevanceExampleSelector:相关性加多样性
MMR 在保证示例与查询相关的同时,还惩罚示例之间的相似度,避免选出的几条示例高度同质化。核心公式是λ * sim(Di, Q) - (1-λ) * max sim(Di, Dj),lambda_mult越大越重视相关性,越小越重视多样性。参数里fetch_k默认先检索 20 个候选,再从中筛出 k 个。
import os from langchain_chroma import Chroma from langchain_community.embeddings import DashScopeEmbeddings from langchain_core.example_selectors import MaxMarginalRelevanceExampleSelector from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate examples = [ {"input": "happy", "output": "sad"}, {"input": "good", "output": "bad"}, {"input": "bright", "output": "dark"}, {"input": "positive", "output": "negative"}, {"input": "up", "output": "down"}, ] example_prompt = PromptTemplate( input_variables=["input", "output"], template="Input: {input}\nOutput: {output}", ) embeddings = DashScopeEmbeddings( model="text-embedding-v3", dashscope_api_key=os.getenv("DASHSCOPE_API_KEY"), ) example_selector = MaxMarginalRelevanceExampleSelector.from_examples( examples, embeddings, Chroma, k=2, ) similar_prompt = FewShotPromptTemplate( example_selector=example_selector, example_prompt=example_prompt, prefix="给出每个输出的反义词", suffix="Input: {adjective}\nOutput:", input_variables=["adjective"], ) print(similar_prompt.invoke({"adjective": "sad"}).to_messages()[0].content)它比纯相似度多一层计算开销,但示例库冗余严重时效果明显更好。调lambda_mult需要一点实验,0.5 是默认起点。
2.4 NGramOverlapExampleSelector:词汇级精确匹配
这个选择器基于 N-gram 重叠度打分,用的是 NLTK 的sentence_bleu变体。它对精确术语极其敏感,print和printf不会被混为一谈,适合代码生成、翻译、拼写纠正这类不能靠同义词匹配的任务。threshold默认 -1.0 表示只排序不排除,设成 0 以上就会过滤掉低分示例。
from langchain_community.example_selectors import NGramOverlapExampleSelector from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate examples = [ {"input": "See Spot run.", "output": "看见Spot跑。"}, {"input": "My dog barks.", "output": "我的狗叫。"}, {"input": "Spot can run.", "output": "Spot可以跑。"}, ] example_prompt = PromptTemplate( input_variables=["input", "output"], template="Input: {input}\nOutput: {output}", ) example_selector = NGramOverlapExampleSelector( examples=examples, example_prompt=example_prompt, threshold=-1.0, ) dynamic_prompt = FewShotPromptTemplate( example_selector=example_selector, example_prompt=example_prompt, prefix="给出每个输入的中文翻译", suffix="Input: {sentence}\nOutput:", input_variables=["sentence"], ) print(dynamic_prompt.invoke({"sentence": "Spot can run fast."}).to_messages()[0].content)它不需要嵌入模型,计算快,但无法识别同义词,长文本的 N-gram 匹配会变得稀疏。适合术语驱动、词序敏感的任务。
四种选择器对比一下:LengthBased 只看长度、无依赖、延迟极低;SemanticSimilarity 看语义、需要嵌入模型和向量库、延迟中等;MMR 看语义加多样性、依赖同上、延迟较高;NGramOverlap 看词汇重叠、依赖 NLTK、延迟低。选哪个取决于你的任务对相关性、多样性和精确性的要求。
3. 把 endpoint 与 auth.json 统一到 TaoToken 通道
前面四种选择器里,SemanticSimilarity 和 MMR 都要调嵌入模型,这就涉及鉴权配置。如果你同时用多个模型供应商,Key 和 endpoint 散落在环境变量、auth.json、代码里,401 和代理报错几乎必然出现。统一到 TaoToken 的 API 通道能省掉这层麻烦。
TaoToken 的 API 地址是https://taotoken.net/api,官网是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。先拿到统一 Key,在控制台创建即可。
对于 LangChain 里的嵌入模型,如果你用 OpenAI 兼容接口,可以这样配:
import os from langchain_openai import OpenAIEmbeddings os.environ["OPENAI_API_KEY"] = "你的TaoToken统一Key" os.environ["OPENAI_BASE_URL"] = "https://taotoken.net/api" embeddings = OpenAIEmbeddings( model="text-embedding-3-small", openai_api_key=os.environ["OPENAI_API_KEY"], openai_api_base=os.environ["OPENAI_BASE_URL"], )如果你用的是 Claude Code 或 Codex 这类工具,auth.json的配置要写全三件套:Base URL、Key、Model ID。以 Codex 的auth.json为例,路径通常在~/.codex/auth.json:
{ "OPENAI_API_KEY": "你的TaoToken统一Key", "OPENAI_BASE_URL": "https://taotoken.net/api", "model": "gpt-4o-mini" }注意OPENAI_BASE_URL不要带末尾斜杠,也不要带/v1,LangChain 和多数客户端会自己拼路径。如果你之前配过本地代理,先把HTTP_PROXY、HTTPS_PROXY、ALL_PROXY这几个环境变量清掉,否则请求会先走代理再走 TaoToken,代理没起来就是local proxy failed,代理起来了但转发规则不对就是 401。
Cline MCP 场景下,配置文件里同样要写全 Base URL、Key、Model ID 三项。CC Switch 切换配置时,确认切换后的 endpoint 指向https://taotoken.net/api,而不是残留的旧地址。
一个容易踩的坑:auth.json里的 Key 和代码里os.environ的 Key 不一致。LangChain 读环境变量,Codex 读auth.json,两边各配各的,结果嵌入模型用 A Key、对话模型用 B Key,401 就来了。统一成同一个 TaoToken Key 最省心。
4. 一次请求验证选样与鉴权是否都正常
配置改完之后,别急着跑完整链路,先用一个最小请求验证两件事:鉴权通不通、选样对不对。
import os from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_chroma import Chroma from langchain_core.example_selectors import SemanticSimilarityExampleSelector from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate os.environ["OPENAI_API_KEY"] = "你的TaoToken统一Key" os.environ["OPENAI_BASE_URL"] = "https://taotoken.net/api" embeddings = OpenAIEmbeddings(model="text-embedding-3-small") examples = [ {"input": "happy", "output": "sad"}, {"input": "tall", "output": "short"}, {"input": "sunny", "output": "gloomy"}, {"input": "windy", "output": "calm"}, ] example_prompt = PromptTemplate( input_variables=["input", "output"], template="Input: {input}\nOutput: {output}", ) selector = SemanticSimilarityExampleSelector.from_examples( examples, embeddings, Chroma, k=2, ) picked = selector.select_examples({"adjective": "cold"}) print("选中的示例:", picked) prompt = FewShotPromptTemplate( example_selector=selector, example_prompt=example_prompt, prefix="给出每个输入的反义词", suffix="Input: {adjective}\nOutput:", input_variables=["adjective"], ) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) chain = prompt | llm result = chain.invoke({"adjective": "cold"}) print("模型输出:", result.content)跑通的话,你会先看到选中的示例打印出两条和 cold 语义最近的示例,然后模型输出返回对应的反义词。如果第一步select_examples就报 401,说明嵌入模型的 Key 或 endpoint 有问题;如果第一步过了、第二步chain.invoke报 401,说明对话模型的配置有问题。这样就能把问题定位到具体是哪一段鉴权。
验证模型本身是否可用,可以直接在模型对话页面发一条测试消息,确认 Key 有效。长期做编码和 Agent 的话,Coding Plan 更适合高频调用场景。
5. 本篇常见报错对照排查
401 Unauthorized:最常见。先检查OPENAI_API_KEY和auth.json里的 Key 是不是同一个 TaoToken Key。再检查OPENAI_BASE_URL是不是https://taotoken.net/api,有没有多写/v1或末尾斜杠。如果 Key 是从控制台复制的,注意有没有带空格。
local proxy failed:环境变量里残留了代理配置。执行env | grep -i proxy看一下,把HTTP_PROXY、HTTPS_PROXY、ALL_PROXY都 unset 掉再跑。这个报错和 TaoToken 本身无关,是本地代理没起来或转发规则不对。
Error reading choices / reading choices:通常是响应体不是预期的 JSON 结构,多半是 endpoint 拼错打到了非 API 路径,或者代理返回了 HTML 错误页。确认 Base URL 是https://taotoken.net/api,不要带多余路径。
OAuth 相关报错:如果你用的是 Claude Code 或 Codex 的 OAuth 流程,确认auth.json里写的是 API Key 模式而不是 OAuth token 模式。三件套 Base URL、Key、Model ID 缺一不可,Model ID 写错也会报鉴权失败。
选样结果不符合预期:如果select_examples返回的示例和输入明显不相关,先确认嵌入模型和对话模型用的是同一个供应商的向量空间。混用不同供应商的嵌入模型会导致相似度计算失真。另外k设太大也会引入噪音,从 2 到 4 开始试。
Chroma 相关报错:langchain-chroma没装或者版本不匹配。执行pip install -U langchain-chroma升级。如果向量库持久化路径冲突,换个目录或者用内存模式先验证逻辑。
排查顺序建议:先确认 Key 和 endpoint,再确认代理环境变量,最后看选样逻辑。大部分 401 都是配置问题,不是代码问题。
6. 把统一 Key 接进你的 LangChain 工作流
示例选择器解决的是「选哪些示例」的问题,统一 Key 解决的是「能不能调通」的问题。两件事分开排查,效率会高很多。我的习惯是先用一个最小脚本验证鉴权,确认嵌入和对话都能通,再把选择器接进完整的 Few-Shot 链路。
如果你还在用多套 Key 分别管嵌入和对话,建议尽早统一到 TaoToken 的 API 通道。接入文档里有各客户端的详细配置示例,API Keys 页面可以创建和管理统一 Key。验证模型是否可用直接去模型对话页面发一条消息就行。长期跑编码和 Agent 任务的话,Coding Plan 的额度模型更适合高频调用。
最后留一个实用技巧:把OPENAI_BASE_URL和OPENAI_API_KEY写进项目的.env文件,用python-dotenv加载,避免每次手动 export。auth.json里的配置和.env保持同一个 Key,这样 LangChain 和 Codex 两边不会打架。选样逻辑调好之后,记得把k和lambda_mult记在注释里,下次换示例库时不用重新试。