前言 在上一篇文章 中,我们探讨了如何自定义LLM
类。但是看到最新的LangGraph
版本中,主要利用的是OpenAI
或者ChatOpenAI
,并使用了ChatOpenAI
独有的bind_tool
方法,使得图结构有了更为丰富的动作与功能,这让我非常眼红。于是本文就探讨了OpenAI
或者ChatOpenAI
包装自定义LLM
的方法。
OpenAI为什么可以? 有些人已经比较熟悉了,为什么偏偏包括OpenAI
在内的大模型厂商都可以使用url
+api-key
的模式访问信息并获取返回结果。这里还是再度解析一下。
我们找到Python
文件中的from openai import OpenAI
,并选择进入OpenAI
看一看原理,我们可以发现,OpenAI
类继承自SyncAPIClient
,而SyncAPIClient
继承自BaseClient[httpx.Client, Stream[Any]]
,类内有一个成员变量:_client: httpx.Client
。
核心的部分在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def request ( self, cast_to: Type [ResponseT], options: FinalRequestOptions, remaining_retries: Optional [int ] = None , *, stream: bool = False , stream_cls: type [_StreamT] | None = None , ) -> ResponseT | _StreamT: if remaining_retries is not None : retries_taken = options.get_max_retries( self.max_retries ) - remaining_retries else : retries_taken = 0 return self._request( cast_to=cast_to, options=options, stream=stream, stream_cls=stream_cls, retries_taken=retries_taken, )
而_request
中,有一段是这样的
1 2 3 4 5 6 7 8 9 10 response = self._client.send( request, ( stream=stream or self._should_stream_response_body( request=request ) ), **kwargs, )
其中,send
方法都并不需要太关注源码,直接看注释都明白是在干啥了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def send ( self, request: Request, *, stream: bool = False , auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, ) -> Response: """ Send a request. The request is sent as-is, unmodified. Typically you'll want to build one with `Client.build_request()` so that any client-level configuration is merged into the request, but passing an explicit `httpx.Request()` is supported as well. See also: [Request instances][0] [0]: /advanced/clients/#request-instances """
也就是说,OpenAI
的本质就是一个巨大的网络请求客户端,而url
与api-key
就是访问客户端的两个参数。
OpenAI怎么传参? 那么知道了这些之后,我们又应该怎么找规律,从而把自己的url
和api-key
塞进去呢?
首先我们注意到其中有一个方法:
1 2 3 4 5 @property @override def auth_headers (self ) -> dict [str , str ]: api_key = self.api_key return {"Authorization" : f"Bearer {api_key} " }
这个和MindIE
的使用方法相当一致。我们接着找,然后在这里找到了一点蛛丝马迹:
1 2 3 4 5 6 7 8 9 10 @property def default_headers (self ) -> dict [str , str | Omit]: return { "Accept" : "application/json" , "Content-Type" : "application/json" , "User-Agent" : self.user_agent, **self.platform_headers(), **self.auth_headers, **self._custom_headers, }
在这里,self.auth_headers
中构建的字典在这里被展开了,成为了新字典中的若干个字段。如果你对HTTP
请求略有基础,你会发现,原来内置的部分属性Accept
、Content-Type
、User-Agent
都是仅出现在请求头 中的内容。也就是说,这个api-key
实际上是应当放在请求头里面一起携带过去。
而url
最终也将成为BaseClient
类中的一个参数,传入httpxRequest._models.Request
类:
1 2 3 4 5 6 7 8 9 10 11 12 return Request( method, url, content=content, data=data, files=files, json=json, params=params, headers=headers, cookies=cookies, extensions=extensions, )
OpenAI怎么兼容? 既然大概知道了OpenAI
的原理,那么关键就是兼容自己的LLM
了。
首先,我们需要一个api
的url
,也就是请求返回结果是按照OpenAI
接口格式返回的一个数据接口,一般情况下是以v1/chat/completions
为结尾的。
然后,我们需要一个api
的key
,这个key
必须得是Bearer
开头的,否则就没办法兼容OpenAI
了,除非你愿意花这个时间把整个OpenAI
的接口实现一遍,然后在自定义api_key
的时候做出不一样的行为。
然后,我们就可以开开心心地访问了:
1 2 3 4 llm = OpenAI( base_url="<your-api-url>" , api_key="Bearer <your-api-key>" )
如果你比较细心的话,你会发现通义千问的方法也是这个:
1 2 3 4 llm = OpenAI( base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" , api_key="<your-dashscope-key>" )
这也反向证明了我们的正确性。
ChatOpenAI如何兼容? 我们同样从源码中找到答案。
首先,找到from langchain_openai import ChatOpenAI
,其实这也是同等于from langchain_openai.chat_models.base import ChatOpenAI
。
在这里,我们找到有一个client
属性与root_client
属性。其中:
1 2 3 4 self.root_client = openai.OpenAI( **client_params, **sync_specific ) self.client = self.root_client.chat.completions
看出来了吗?ChatOpenAI
本质上就是openai.OpenAI
的封装,root_client
属性就是openai.OpenAI
的实例,client
属性是v1/chat/completions
接口的实例。
这就没什么悬念了。所以ChatOpenAI
实际上与OpenAI
一样,只需要这样:
1 2 3 4 5 llm = ChatOpenAI( base_url="<your-api-url>" , api_key="Bearer <your-api-key>" , model="<your choice>" )