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 拼消息,全都只跟这一个接口打交道。

BaseLlmllms/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,为啥不 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)超时——尤其智能体高频调用时。如果一遇限流就失败,智能体动不动就断。@retry 自动重试 + 指数退避(等待时间越来越长,避免"越限流越猛打")——遇到限流/超时自动等一会再试,最多 5 次。这是生产级 LLM 调用的必备防御。面对不稳定的外部依赖(LLM API),自动重试是韧性的关键。
📞 生活类比:就像打电话占线自动重拨——第一次没打通不会直接放弃,而是等一会再拨,且越等越久(避免一直猛拨反而更堵)。最多拨 5 次还不通才认输。
L04

适配器抹平差异(精髓)

各家 API 长得不一样。GooglePalmllms/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 BaseLlm(接口)messages ↔ 统一字典 OpenAimessages → ChatCompletion.create GooglePalm(适配)messages "压平"成 prompt 字符串→ palm.generate_text Replicate→ replicate.run LocalLLM→ 本地 /v1/chat 接口
调用方永远用 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_modelllms/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 喂给不同厂商 → 出参一模一样(输入 → 输出) 输入(无论哪家都一样):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._executeresult["content"]、Day 09 判断失败——都依赖这个统一格式。统一入参(messages)+ 统一出参(字典)= 调用方彻底不用管厂商差异。这是抽象层给上层的最大礼物。
L07

token 计数

TokenCounterhelper/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 装饰器把普通函数变工具。
← Day 09 输出解析 Day 11 · 工具深入 →