Day 10 / 共 20 天 · 第 2 周 Agent 执行(收官)
LLM 抽象
第2周收官。大模型是智能体的"大脑"。今天读 llms/:BaseLlm 统一接口、适配器抹平各家 API 差异、工厂选实现、自动重试、token 计数。
📍 你在整门课的位置 · 第 2 周「Agent 执行」收官
D1-5 核心概念→
D6 执行循环→
D7 工作流→
D8 提示词→
D9 输出解析→
D10 LLM抽象🎓⇢
D11-15 工具/记忆
L01
BaseLlm 统一接口
🤔 直接
import openai 调 GPT 不就完了?为什么还要套一层 BaseLlm 抽象?
因为如果迭代步、每个工具都 import openai 写死,哪天想换成本地模型 / 文心 / Claude,就得改几十处代码,还得处理各家 API 长得完全不一样(OpenAI 吃消息数组、Palm 只吃一段 prompt 字符串)。💡 一句话本质:面向接口而非实现(把"用哪个模型"和"怎么用"解耦)
业务只依赖
BaseLlm.chat_completion(messages)——统一入参(消息数组)、统一出参(字典)。各家 API 的差异全被封在各自实现的 chat_completion 里(适配器模式)。换模型 = 注入不同的 BaseLlm 子类(工厂选),业务代码一行不改。Day 06 迭代步、Day 09 判断成败、Day 08 拼消息,全都只跟这一个接口打交道。BaseLlm(llms/base_llm.py:4)是纯抽象基类,所有厂商实现(OpenAI/Palm/Replicate/HuggingFace/本地)必须实现这几个方法:
class BaseLlm(ABC):
@abstractmethod
def chat_completion(self, prompt): ... # ★ 核心:发消息、拿回复
@abstractmethod
def get_source(self): ... # 厂商标识
@abstractmethod
def get_model(self)/get_models(self): ...
@abstractmethod
def verify_access_key(self): ... # 校验密钥
读法:核心就一个
chat_completion(messages)——发一组消息、拿回复。Agent 和工具(Day 03 的 ThinkingTool 等)只依赖 BaseLlm,从不直接 import openai——换厂商时业务代码一行不改。"面向接口"的经典收益
这是策略模式:
🔌 生活类比(贯穿今天):
BaseLlm 是接口,多个厂商是可替换的策略。Agent 逻辑里写 self.llm.chat_completion(...)——它不知道也不关心背后是 GPT-4 还是 Palm 还是本地模型。想换模型?注入不同的 BaseLlm 实现即可。这和 eino/CrewAI/AutoGPT 的 LLM 抽象完全同构——五个框架都用"统一 LLM 接口"来解耦"用哪个模型"。🔌 生活类比(贯穿今天):
BaseLlm 就像墙上的国标插座——不管你插的是台灯、充电器还是电脑(GPT / Palm / 本地模型),插座口(chat_completion)长得都一样;电器完全不用关心插座背后接的是水电、火电还是核电。💬 换个对话再想一遍
👶 小白:既然最后都是调 GPT,为啥不
👨🏫 老师:那哪天要换成公司内网的本地模型呢?
👶 小白:把
👨🏫 老师:问题是迭代步、每个工具、token 计数……几十处都 import 了,而且各家 API 调法还不一样(有的吃消息数组、有的只吃一段字符串)。套一层
👶 小白:懂了,是"改一处 vs 改几十处"的区别。
import openai 一把梭,图省事?👨🏫 老师:那哪天要换成公司内网的本地模型呢?
👶 小白:把
import 改掉不就行了?👨🏫 老师:问题是迭代步、每个工具、token 计数……几十处都 import 了,而且各家 API 调法还不一样(有的吃消息数组、有的只吃一段字符串)。套一层
BaseLlm 后,换模型只改"注入哪个实现"这一处。👶 小白:懂了,是"改一处 vs 改几十处"的区别。
L02
OpenAi 实现
# llms/openai.py:73 chat_completion(简化)
def chat_completion(self, messages, max_tokens=...):
response = openai.ChatCompletion.create(
n=self.number_of_results, model=self.model, messages=messages,
temperature=self.temperature, max_tokens=max_tokens, top_p=self.top_p, ...)
content = response.choices[0].message["content"]
return {"response": response, "content": content}
读法:OpenAi 实现把
chat_completion 落到 openai.ChatCompletion.create,返回统一的 {"response":..., "content":...} 字典。构造函数(:20)收集所有模型参数(temperature/max_tokens/top_p/penalty…)。get_models(:131)过滤出支持的模型(gpt-4/gpt-3.5-turbo…)。L03
自动重试退避
chat_completion 上挂着 @retry(tenacity 库,openai.py:62):
@retry(
retry=(retry_if_exception_type(RateLimitError) | Timeout | TryAgain),
stop=stop_after_attempt(5), # 最多 5 次
wait=wait_random_exponential(min=30, max=300)) # 30~300s 指数退避
def chat_completion(self, messages, ...): ...
为什么 LLM 调用必须自动重试?
调 LLM API 经常遇到 限流(RateLimitError)和超时——尤其智能体高频调用时。如果一遇限流就失败,智能体动不动就断。
📞 生活类比:就像打电话占线自动重拨——第一次没打通不会直接放弃,而是等一会再拨,且越等越久(避免一直猛拨反而更堵)。最多拨 5 次还不通才认输。
@retry 自动重试 + 指数退避(等待时间越来越长,避免"越限流越猛打")——遇到限流/超时自动等一会再试,最多 5 次。这是生产级 LLM 调用的必备防御。面对不稳定的外部依赖(LLM API),自动重试是韧性的关键。📞 生活类比:就像打电话占线自动重拨——第一次没打通不会直接放弃,而是等一会再拨,且越等越久(避免一直猛拨反而更堵)。最多拨 5 次还不通才认输。
L04
适配器抹平差异(精髓)
各家 API 长得不一样。GooglePalm(llms/google_palm.py:45)——Palm 用 generate_text + 单个 prompt 字符串,没有"消息数组"概念。它在 chat_completion 里做适配:
🛠️ 如果让你自己支持多厂商(简化版 → 真实版对照)
朴素做法:在业务代码里到处
真实版:把差异下沉到每个
if provider == "openai": openai.ChatCompletion.create(messages=...)
elif provider == "palm": palm.generate_text(prompt=...) # 入参格式还不一样!
问题:每个用 LLM 的地方都要写这坨判断,且各家入参/出参格式不同,业务逻辑被厂商差异污染。真实版:把差异下沉到每个
BaseLlm 子类的 chat_completion 里——对外统一 messages 进、统一字典出。业务只写 self.llm.chat_completion(messages),一个 if 都不用。def chat_completion(self, messages, ...):
# 把 OpenAI 风格的 messages 数组"压平"成一段 prompt 文本
prompt = "\n".join(["`" + m["role"] + "`: " + m["content"] for m in messages])
completion = palm.generate_text(model=..., prompt=prompt, ...)
return {"response": completion, "content": completion.result} # 仍返回统一格式
调用方永远用
chat_completion(messages);OpenAI/Palm/Replicate/本地各家 API 的差异,全被封在各自实现里(Palm 还得把消息数组压平成 prompt)。🎯 这是整个抽象层的精髓
Palm 的原生 API 和 OpenAI 完全不同(Palm 只吃一段 prompt 字符串,不吃消息数组)。但
chat_completion(messages) 的入参格式(消息数组)和出参格式(统一字典)被 SuperAGI 强行统一——Palm 内部把消息数组"压平"成带角色标注的 prompt,对外却和 OpenAI 表现一致。这就是适配器(Adapter)模式:调用方永远用同一套方式,各家 API 的差异全被封在各自实现的 chat_completion 里。换厂商,业务零改动。L05
工厂选实现
get_model(llms/llm_model_factory.py:12)——按 provider 名返回对应实现:
def get_model(organisation_id, api_key, model="gpt-3.5-turbo", **kwargs):
provider_name = 查 DB 确定该模型属于哪个 provider
if provider_name == 'OpenAI': return OpenAi(...)
elif provider_name == 'Replicate': return Replicate(...)
elif provider_name == 'Google Palm': return GooglePalm(...)
elif provider_name == 'Local LLM': return LocalLLM(...)
# 返回类型统一是 BaseLlm
读法:工厂方法——调用方只给"模型名 + 密钥",工厂查 DB 确定 provider、返回对应的 BaseLlm 子类。拿到手后调用方不用管是哪家(返回类型都是 BaseLlm)。Day 03 的
ToolBuilder 就是通过 get_model 拿 LLM 再注入给工具的 tool.llm——工具系统和 LLM 抽象在这里闭环。L06
统一返回格式
所有实现的 chat_completion 都返回 {"response":..., "content":...};出错时返回 {"error":..., "message":...}。
📝 同一段 messages 喂给不同厂商 → 出参一模一样(输入 → 输出)
输入(无论哪家都一样):
→ 走 OpenAi 实现:内部调
→ 走 GooglePalm 实现:内部把 messages 压平成 prompt 调
→ 调用方统一处理:取文本
messages=[{"role":"system","content":"你是助手"},{"role":"user","content":"你好"}]→ 走 OpenAi 实现:内部调
ChatCompletion.create → 返回 {"response": <原始对象>, "content": "你好,有什么可以帮你?"}→ 走 GooglePalm 实现:内部把 messages 压平成 prompt 调
generate_text → 仍返回 {"response": ..., "content": "你好呀!"}→ 调用方统一处理:取文本
result["content"];判失败 if "error" in result——完全不用管背后是哪家模型。统一返回格式的价值
调用方(工具、迭代步)拿到结果,只需:想要文本读
result["content"]、判断成败看 'error' in result——不管底层是哪家模型,处理方式完全一样。回忆 Day 03 的 ThinkingTool._execute 里 result["content"]、Day 09 判断失败——都依赖这个统一格式。统一入参(messages)+ 统一出参(字典)= 调用方彻底不用管厂商差异。这是抽象层给上层的最大礼物。L07
token 计数
TokenCounter(helper/token_counter.py:11)用 OpenAI 的 tiktoken 数 token。count_message_tokens(:37)按官方规则给每条消息加固定开销;count_text_tokens(:85)数纯文本;token_limit(:17)查各模型上下文上限。
为什么处处要数 token?
LLM 有硬性上下文长度限制(比如 8K/32K token)且按 token 计费。框架在拼 prompt、塞历史、截断工具输出前,都要先算清楚 token 数——避免超限报错(内容塞不下)、避免浪费钱(塞太多)。Day 08 的滚动记忆压缩(超预算才压缩)、Day 03 工具的 max_token_limit(限制输出),都靠 TokenCounter 算。token 是 LLM 应用的"计量单位",管好它是成本和可靠性的基础。
L08
🎓 第 2 周收官 + 动手
第 2 周(Day 06-10)你已吃透 Agent 执行
- Day 06 执行循环:两级 step、think→act→observe
- Day 07 工作流:数据驱动有向图、条件路由、LLM 当路由器
- Day 08 提示词构建:模板+变量、滚动记忆压缩
- Day 09 输出解析:AgentGPTAction、容错、自我纠错
- Day 10 LLM 抽象:统一接口、适配器、工厂、重试、token
你已理解智能体"怎么想、怎么被驱动、怎么调模型"。下周(Day 11-15)进入 工具/记忆/资源——工具体系、工具市场、向量记忆、资源管理、数据模型。
✋ 动手
P=superagi/llms
cat base_llm.py
sed -n '62,115p' openai.py # @retry + chat_completion
sed -n '45,75p' google_palm.py # 适配器抹平差异
sed -n '12,55p' llm_model_factory.py # 工厂
sed -n '37,98p' ../helper/token_counter.py # token 计数
下周预告 · Day 11:深入 工具体系——base_tool 完整、Toolkit、真实工具精读、@tool 装饰器把普通函数变工具。