LangChain 接入小鱼API:从 ChatOpenAI 到 RAG 与 Agent

最小可用代码

LangChain 接小鱼API 只需要在 ChatOpenAI 里补一个 base_url,其余写法与上游文档完全一致。下面这段是能直接跑起来的最小版本:

# pip install -U langchain langchain-openai
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="deepseek-v3.2-thinking",
    api_key="YOUR_KEY",
    base_url="https://xyuapi.top/v1",
    temperature=0.7,
    timeout=120,
    max_retries=2,
)

msg = llm.invoke("用三句话说明什么是向量数据库")
print(msg.content)

关键改动只有两处

api_key 换成 xyuai.cc 后台生成的令牌,base_urlhttps://xyuapi.top/v1。注意 LangChain 里 base_url 是顶层参数,不是塞进 model_kwargs 里;塞错位置不会被报错,只会静默走默认地址,然后返回 401。

跑通之后先做这件事

先用三条真实业务提示词各跑一次,确认输出格式和原来一致。很多人迁移完只跑了一句「你好」就宣布成功,结果上线后才发现长输出的模型名不一致、流式解析出错。三条真实数据的验证成本很低,能挡掉后面大部分返工。

安装与环境准备

版本怎么选

LangChain 的包结构在这两年做过一次大拆分:核心逻辑进了 langchain-core,各家模型集成各自独立发包。现在装东西要分清三层,langchainlangchain-corelangchain-openai。只装 langchain 而不装 langchain-openai,导入 ChatOpenAI 时会直接报模块不存在。

安装命令

pip install -U langchain langchain-openai langchain-community
pip install -U langchain-text-splitters        # 文档切分单独一个包
pip show langchain langchain-openai | findstr Version

版本上有个硬约束:langchain-openai 的主版本号要和 langchain-core 匹配,混装新旧版本会出现 ImportError: cannot import name 'BaseChatModel' 这类看起来莫名其妙的错误。升级时三个包一起升。

密钥放哪里

$env:OPENAI_API_KEY="YOUR_KEY"
$env:OPENAI_BASE_URL="https://xyuapi.top/v1"

ChatOpenAI 会自动读取这两个环境变量,代码里就只用写 model 一个参数。放进环境变量而不是写死在源码里,能避免误提交到代码仓库后被爬虫扫走。

ChatOpenAI 参数逐项说明

base_url 与 api_key

参数该怎么填常见错误
base_urlhttps://xyuapi.top/v1漏写 /v1,或写成 /v1/ 结尾
api_key后台生成的令牌复制时带换行或空格
model模型列表里的完整名字用别家平台的简称
timeout120 秒起步沿用默认值导致长回答超时
max_retries2 到 3设成 0,遇到 429 直接失败

model 与 temperature

model 必须是字符串,写错名字会返回 404,LangChain 包装后会抛 NotFoundErrortemperature 的可接受范围随模型而变,思考型模型多数只接受默认值,传了 temperature=1.5 之类的越界值会被上游拒绝。

timeout 与 max_retries

思考型模型首字延迟高,三千字的长回答可能要等四十秒。timeout 设 30 秒的表现是偶发成功、偶发超时,很难定位。建议对话类 120 秒,长文类 300 秒。max_retries 只对 429 和 5xx 生效,超时不重试,所以业务层还要再包一层。

用哪个参数控制输出长度

max_tokens 直接决定单次回复的上限。设得太小,长回答会在中途被切断,finish_reason 返回的是长度上限而不是正常结束;设得偏大没有坏处,模型不会因为上限高就多写。按模型实际支持的范围给一个宽松值即可,比如 8192。

用 Pydantic 约束返回格式

from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(description="商品名称")
    price: float = Field(description="价格,单位元")
    tags: list[str] = Field(description="不超过 5 个标签")

structured = llm.with_structured_output(Product)
item = structured.invoke("从这段描述里抽取商品信息:小熊保温杯,售价 59.9 元,304 不锈钢,送礼首选")
print(item.name, item.price)

with_structured_output 走的是函数调用通道,比让模型自己输出 JSON 再解析可靠得多。字段描述要写清楚,模型是按描述来填的。

参数兼容性怎么判断

不同模型对参数的宽容度不一样,报 400 时先做一件事:把模型换成 gemini-2.5-pro 再试一次,能通就说明参数本身没错,是模型侧的限制。这样可以把「代码问题」和「模型差异」快速分开。

Prompt 模板与链

ChatPromptTemplate 的写法

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是电商客服助手,回答不超过 100 字,不承诺具体到货时间。"),
    ("human", "用户问题:{question}\n订单状态:{status}"),
])

变量名用花括号占位,invoke 时传字典。变量名拼错会在调用时报 KeyError,比模型返回错误更容易排查。

用管道符把链串起来

from langchain_core.output_parsers import StrOutputParser

chain = prompt | llm | StrOutputParser()
answer = chain.invoke({"question": "什么时候发货", "status": "已付款待发货"})
print(answer)

管道符本质是 Runnable__or__ 重载,左边的输出喂给右边当输入。任何实现了 Runnable 接口的对象都能串进去,包括检索器、解析器、自定义函数。

链的中间结果怎么调试

在链路中间插一个 RunnableLambda 把中间值打印出来即可,或者调用 chain.invoke(..., config={"callbacks": [handler]}) 传一个回调处理器。常见做法是先单独跑 prompt.invoke({...}) 看渲染出来的消息,确认占位符替换没问题,再把整条链接上。

流式输出与回调

stream 模式

for chunk in llm.stream("写一段 300 字的商品卖点"):
    print(chunk.content, end="", flush=True)

stream 返回的是迭代器,直接进 for 就能逐字打印。链式结构上也可以 .stream(),LangChain 会自动沿管道向后传递流式片段,但要求链路里每一环都支持流式,中间插了不支持流式的处理器就会退化成一整块输出。

astream_events 做打字机效果

async for event in chain.astream_events({"question": "介绍一下会员权益"}, version="v2"):
    if event["event"] == "on_chat_model_stream":
        piece = event["data"]["chunk"].content
        if piece:
            print(piece, end="", flush=True)

version="v2" 不能省,不写会直接抛异常。事件名里 on_chat_model_stream 是模型吐字的信号,想抓工具调用就用 on_tool_starton_tool_end

流式中断怎么处理

流式请求不会自动重连,连接中途断掉时迭代器直接结束,不会抛异常。稳妥做法是记录已收到的文本长度,一旦在正常结束标记出现之前就退出循环,就带着已生成内容重新请求一次,让模型续写,重试上限设两次。

流式与结构化输出能不能一起用

可以,但要求链路里每一环都支持流式。中间插一个必须拿到完整输入才能工作的解析器,整条链就会退化成一整块输出,前端看到的效果是文字从头到尾一次性出现。真遇到这种情况,把那个环节挪到链路末端就行。

RAG:向量检索与问答

文档切分

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=120,
    separators=["\n\n", "\n", "。", "!", "?", " ", ""],
)
chunks = splitter.split_documents(docs)

chunk_overlap 不要设成 0,段落被硬切时上下文会丢。中文场景记得把中文标点加进分隔符列表,否则会按空格乱切。

向量化怎么做

向量化有两条路:本地模型和接口。本地用 BAAI/bge-small-zh-v1.5 这类中文小模型,机器上就能跑,数据不出本机;接口方式则用平台的向量化型号,具体名字以后台模型列表为准,写错同样返回模型不存在。文档量大时本地跑更省,检索延迟也更可控。

接向量库做检索问答

from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

store = FAISS.from_documents(chunks, embeddings)     # embeddings 用上面任一方案
retriever = store.as_retriever(search_kwargs={"k": 4})

rag = (
    {"context": retriever | (lambda ds: "\n\n".join(d.page_content for d in ds)),
     "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

提示词里要明确写一句「只根据提供的资料回答,资料里没有的信息直接说不知道」,否则模型会拿自己的记忆补全,看着像答对了,其实是编的。

Agent 与工具调用

定义一个工具

from langchain_core.tools import tool

@tool
def query_price(model_name: str) -> str:
    """查询指定模型名的一次调用单价,单位元。"""
    table = {"gemini-2.5-pro": "0.031", "deepseek-v3.2-thinking": "0.049"}
    return table.get(model_name, "未收录该型号")

函数的 docstring 就是给模型看的工具说明,写得含糊模型就不会正确调用。参数类型标注也是必需的,模型靠它生成参数。

绑定工具并让它自己调

llm_with_tools = llm.bind_tools([query_price])
msg = llm_with_tools.invoke("gemini-2.5-pro 一次多少钱?")

if msg.tool_calls:
    for call in msg.tool_calls:
        print("模型想调用:", call["name"], call["args"])

bind_tools 只负责把工具定义发给模型,真正的执行和回填要自己写循环:把工具的返回结果包成 ToolMessage 追加进消息列表,再请求一次模型。LangChain 也提供 create_agent 这类封装好的执行器来自动完成这个循环,不同大版本的工厂函数名不一样,升级时留意一下导入路径。

Agent 循环里的坑

一是循环没有出口,模型反复调同一个工具,必须设最大步数上限。二是工具抛异常直接打断整个流程,工具内部要把异常兜住并返回一句可读的失败说明,让模型有机会换路径。三是每轮循环都会产生一次计费调用,按次计费下这个成本增长是线性的,跑之前先估算步数。

常见坑与排查

现象原因处理
401 认证失败base_url 没生效,或令牌带空格打印实际地址,strip() 令牌
404 模型不存在模型名写了别家的简称对照后台模型列表逐字核对
400 参数不支持传了该模型不接受的参数去掉温度、top_p 等可选参数
首字很慢后超时timeout 过短提到 120 秒,长任务 300 秒
429 频率限制并发过高降到 3 到 5 并发,加指数退避
导入报错三个包版本不匹配三个包一起升到同代版本

base_url 写错位置

base_url 塞进 model_kwargs={"base_url": ...} 是最隐蔽的一类错误,代码不报错,请求照样发出去,只是发到了别的地方。判断方法是打印 llm.openai_api_base 看实际值。

模型名与参数不兼容

同一个提示词在 A 模型上正常、在 B 模型上报 400,原因往往在参数上而不是提示词。思考型模型常拒绝 temperaturetop_p,把这两个参数去掉再试。

首字延迟与超时

timeout 在 LangChain 里是整次请求的时限,包含排队、推理和输出三个阶段。长输出的任务要按「最慢情况」设,而不是按平均值设。超时后 max_retries 不会重试,需要业务层自己处理。

依赖版本冲突

langchain 生态包多,最容易出现的是装了两个不同主版本的 langchain-corepip check 能查出这类冲突,pip list | findstr langchain 能看清实际装了哪些包、什么版本。

成本与模型选择

常用模型的单价

模型名单价适合的环节
gemini-2.5-pro0.031 元/次批量分类、初筛、简单问答
deepseek-v3.2-thinking0.049 元/次中文生成、结构化抽取
claude-sonnet-4-5-thinking0.09 元/次长文写作、多步链式任务
gpt-5.50.2 元/次难题推理、Agent 主控

全部按调用次数计费,与输入输出长度无关。这一点在 RAG 场景里特别重要:把整篇长文档塞进上下文,和只塞四段检索结果,价格是一样的。

给不同环节配不同模型

一条链里不必从头到尾用同一个模型。检索改写、意图分类这类环节用 gemini-2.5-pro 就够,生成环节用 deepseek-v3.2-thinkingclaude-sonnet-4-5-thinking,只有真正需要多步推理的 Agent 主控才上 gpt-5.5。这样拆完之后,同样的链成本能降下来一截,质量反而更稳,因为每个环节用的都是它擅长的那一档。

小结

把 LangChain 接到小鱼API,实质上就是在 ChatOpenAI 里多填一个 base_url,剩下的提示词模板、链、检索器、工具调用全都按原有方式写。真正需要留神的是三类不报错的错误:参数放错位置、超时设得太短、依赖包版本不匹配。这三类各自都有一个几分钟就能做完的确认动作,提前做掉比线上排查省事得多。

相关阅读

🚀 想要立即使用?来小鱼API体验全系列AI模型

查看全部产品

支持Gemini / Claude / GPT / Grok / DeepSeek · 国内直连 · 支付宝/微信