在兼容 OpenAI 的 API 上跑 LangChain
ChatOpenAI 只要改 base_url 和 api_key 两个字段就能连上这里。invoke、stream、工具调用和结构化输出全都原样通过,因为网关提供的就是 OpenAI 的 wire format。文章还讲了四个别名怎么选,以及缓存读取按输入费率十分之一计费如何让 agent 的循环一直便宜。
LangChain 并不需要知道它在跟我们说话。它的 OpenAI chat model 从自己的构造函数里读取 base URL,而这个网关按 OpenAI 的 wire format 应答,所以一条为 OpenAI 写的 chain,改两个参数就能在这里跑起来。下面写的没有一样是 langchain-openai 的分支、包在它外面的一层壳,或者打过补丁的副本:就是你手上那个包,只是配置换了。
真正要改的两个字段
ChatOpenAI 的参数列表很长,绝大多数保持原样就好。只有两个不是 — 它发往的地址,和它带上的 key:
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="terra",
base_url="https://aifromu.com/v1",
api_key=os.environ["AIFROMU_API_KEY"],
)
print(llm.invoke("Name three uses for a cache read.").content)
上面那个 base URL 是本页打印出来的,不是手写进去的。它就是这套部署实际对外服务的地址,所以这段代码对着你此刻读它的那个 host 就能跑,而不是对着某个人很久以前编进示例里的固定主机名。key 从 environment 里取,理由和别处一样:写进构造函数里的 key 就是已经进了 repository 的 key,之后想轮换它,等于改代码,而不是改一个变量。
这个对象的其余部分全是库自己的。invoke 返回完成的 message,.content 就是正文;传一个 message 列表和传一个裸字符串一样行得通;ChatPromptTemplate | llm | StrOutputParser() 的拼装方式一点没变。这些都不是我们有权改动的东西 — 而这正是提供别人的 wire format、而不是自己另发明一套的全部理由。
流式输出、工具调用和结构化输出
llm.stream("...") 会在模型一边写的时候把答案一块一块交给你。server-sent events 穿过这个网关直达你的进程,所以你围着它们写的那个循环,还是库自己的循环:
for chunk in llm.stream("Explain a cache read in two sentences."):
print(chunk.content, end="", flush=True)
只要内容长,就用它。一次跑上几分钟的生成,必须在某条闲置连接被你的进程和模型之间的某个环节掐断之前,先往线上送出点什么,而送出这点什么的就是流式输出 — 流式输出那篇指南讲了这条界线具体落在哪里,以及该拿它怎么办。
工具调用和结构化输出在这里没有单独一节,而这恰恰是重点。bind_tools 和 with_structured_output 拼出来的还是它们一向拼的那些请求体,网关提供的就是 OpenAI 的 wire format,回来的答案也还是 LangChain 早就会解析的那个形状。没有什么要移植,也没有什么要绕开。
该填哪个模型名
model= 接受四个别名之一,按你要做的事来选,而不是按模型是谁家的:astra 用于 100 万 token 窗口上的前沿推理,sol 用于深度推理和难写的代码,terra 用于日常助手类工作 — RAG、agent、工具调用和写代码,上面那段代码里用的就是它 — luna 用于快速草稿、分类和大批量对话。
一条本来就在发 OpenAI 风格模型名的 chain,不必一个调用点一个调用点地改。那些名字在链路上会被接受,并按它们所属的重量级来理解:带 mini 或 nano 的名字是某人的廉价高频调用,落在 luna 上;gpt-6 这一类的名字是今年的前沿,落在 astra 上;普通的 4 系列在两者之间,落在 terra 上。回复里回传的仍是你发出的那个字符串,所以你的客户端依旧认得出这是它自己的请求;而一个哪条规则都不匹配的名字会被拒绝,不会去猜 — 猜一次就是同时朝两个方向算错钱,而这两个方向都要等有人真去读账目才看得见。
会循环的 chain,和缓存读取
agent 就是一个循环,而循环会把自己的前缀反复发出去。agent executor 的每一轮,都会把 system prompt、工具定义和到目前为止的全部对话记录再顺着链路送上去一遍;一条在每个检索到的段落前面都放同一段说明的 RAG chain,做的是同一件事,只是规模小一些。真正吃掉一条长跑 chain 大部分开销的,是这段前缀,而不是答案本身。
而它也正是重发起来便宜的那部分。从缓存前缀里读回来的 token,按所属模型输入费率的十分之一计费,所以一段每一轮都逐字节保持不变的 system prompt,正是让 agent 循环一直便宜下去的东西。消息的顺序就照这个来排:永远不变的部分放最前面,检索到的文档和用户这一轮的输入放在后面。
这些都不需要你凭信任接受。控制台会把每一笔请求逐项列出来,省下来的钱就出现在你看得见的地方 — 一轮一轮地,对着花掉它的那次运行核。
从缓存前缀里读回来的 token,按所属模型输入费率的十分之一计费,所以一段每一轮都逐字节保持不变的 system prompt,正是让 agent 循环一直便宜下去的东西。
本文为机器翻译,母语审校待完成。以英文版为准。