从
LiteLLMProvider到ContextBuilder,看 nanobot 如何让 20+ 种模型"说同一种语言"
在前四篇文章中,我们先后剖析了 nanobot 的整体架构、核心循环和插件系统。AgentLoop 负责"思考",插件系统负责"行动",但有一个关键问题尚未解答:Agent 究竟如何与形形色色的 LLM 对话?
不同模型提供商(OpenAI、Anthropic、Google、DeepSeek......)有着各异的 API 格式、认证方式和参数名称。如果为每个模型写一套适配代码,项目很快就会变得臃肿不堪。nanobot 的解决方案是:通过 LiteLLMProvider 统一接口,通过 ContextBuilder 精心构造提示词。
如果把 Agent 比作一个人,那么:
- LiteLLMProvider 是"语言中枢"------无论对方说中文还是英文,都能翻译成统一的内部语言
- ContextBuilder 是"思维整理师"------把所有相关信息整理成清晰的提示,让 LLM 一次看懂
今天,我们就来深入解析这两个核心模块的源码实现。
1. 整体设计:抽象层与构建层的完美配合
先看一张整体架构图,了解 LLM 相关模块在整个系统中的位置:
┌─────────────────────────────────────────────────────────────┐
│ AgentLoop │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ContextBuilder │ │
│ │ • 系统提示(人格设定) │ │
│ │ • 长期记忆(MEMORY.md) │ │
│ │ • 短期记忆(对话历史) │ │
│ │ • 技能摘要(SKILL.md) │ │
│ │ • 当前用户消息 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LiteLLMProvider │ │
│ │ • 统一调用接口(complete()) │ │
│ │ • 多提供商路由(OpenAI/Anthropic/Gemini...) │ │
│ │ • 参数标准化(temperature/max_tokens...) │ │
│ │ • Token计数与预算管理 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LLM Providers │
│ OpenAI │ Anthropic │ Gemini │ DeepSeek │ vLLM │
│ OpenRouter │ Groq │ Zhipu │ ... (20+ 种) │
└─────────────────────────────────────────────────────────────┘
核心设计理念:
- 统一接口 :无论后端是什么模型,AgentLoop 都通过
complete(messages, tools)这一个方法调用 - 配置驱动 :通过
config.json指定使用哪个提供商、哪个模型,无需修改代码 - 上下文组装 :
ContextBuilder负责把分散的信息(记忆、技能、历史)拼成完整的提示词
2. 模型接口封装:LiteLLMProvider 源码解析
2.1 为什么选择 LiteLLM?
LiteLLM 是一个轻量级的 Python 库,提供了统一的接口调用 100+ 种 LLM。它的核心价值在于:
- 统一 API :所有模型都通过
completion()方法调用 - 自动处理认证 :根据模型前缀读取对应的环境变量(如
OPENAI_API_KEY) - 工具调用标准化:将不同提供商的工具调用格式转换为统一的 OpenAI 格式
nanobot 在 providers/litellm_provider.py 中对 LiteLLM 做了进一步封装。
2.2 懒加载机制:优化启动性能
LiteLLM 的导入耗时约 1-2 秒,nanobot 通过懒加载机制避免启动卡顿:
python
# providers/litellm_provider.py (简化版)
import importlib
class LiteLLMProvider:
def __init__(self, config):
self.config = config
self._litellm = None # 延迟加载
def _get_litellm(self):
"""懒加载 LiteLLM 模块"""
if self._litellm is None:
self._litellm = importlib.import_module("litellm")
return self._litellm
async def complete(self, messages, tools=None):
litellm = self._get_litellm() # 首次调用时才导入
# 实际调用...
这种设计将 LiteLLM 的导入成本推迟到第一次真正需要调用 LLM 时,显著提升了 CLI 响应速度和整体启动性能。
2.3 核心方法:complete()
complete() 是 AgentLoop 唯一需要调用的 LLM 接口:
python
async def complete(self, messages, tools=None, tool_choice="auto"):
"""统一的 LLM 调用接口"""
litellm = self._get_litellm()
# 1. 准备调用参数
kwargs = {
"model": self.config.model,
"messages": messages,
"temperature": self.config.temperature,
"max_tokens": self._get_dynamic_max_tokens(messages),
}
# 2. 如果有工具,添加到请求中
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = tool_choice
# 3. 根据模型提供商添加特定参数
self._add_provider_specific_params(kwargs)
try:
# 4. 调用 LiteLLM
response = await litellm.acompletion(**kwargs)
# 5. 标准化返回格式
return self._normalize_response(response)
except Exception as e:
# 6. 错误处理
logger.error(f"LLM 调用失败: {e}")
raise
2.4 动态参数适配
不同提供商支持的参数不同,nanobot 会根据模型前缀动态适配:
python
def _add_provider_specific_params(self, kwargs):
"""根据模型提供商添加特定参数"""
model = kwargs["model"]
if model.startswith("gemini/"):
# Gemini 支持 reasoning_effort
if self.config.reasoning:
kwargs["reasoning_effort"] = "medium"
elif model.startswith("openai/"):
# OpenAI 不支持 top_k/min_p
pass
else:
# 其他提供商可能支持高级采样参数
if self.config.top_k:
kwargs["top_k"] = self.config.top_k
if self.config.min_p:
kwargs["min_p"] = self.config.min_p
这种条件适配确保了 nanobot 既能使用高级参数优化模型输出,又不会因参数不兼容导致调用失败。
2.5 多提供商配置
在 config.json 中,用户可以配置多个提供商:
json
{
"providers": {
"openrouter": {
"apiKey": "sk-or-v1-xxx"
},
"openai": {
"apiKey": "sk-xxx"
},
"anthropic": {
"apiKey": "sk-ant-xxx"
}
},
"agents": {
"defaults": {
"model": "anthropic/claude-3-opus-20240229",
"temperature": 0.7,
"maxTokens": 4096
}
}
}
nanobot 支持的提供商包括:
- OpenRouter:统一访问多种模型(推荐)
- OpenAI:GPT-4、GPT-3.5 系列
- Anthropic:Claude 系列
- Google Gemini:Gemini Pro/Flash
- DeepSeek:DeepSeek 系列
- vLLM:本地部署的开源模型
- Groq 、Zhipu 等
2.6 Token 计数与预算管理
为了防止上下文溢出,nanobot 实现了精确的 token 计数和动态限制:
python
def _get_dynamic_max_tokens(self, messages):
"""根据剩余 token 动态计算响应长度"""
# 1. 计算已用 token
litellm = self._get_litellm()
used_tokens = litellm.token_counter(
model=self.config.model,
messages=messages
)
# 2. 计算剩余 token(留 10% 缓冲)
remaining = self.config.max_tokens - used_tokens
safe_budget = int(remaining * 0.9)
# 3. 响应 token 不能超过剩余的一半(为后续轮次留空间)
max_response = min(
self.config.max_response_tokens,
safe_budget // 2
)
# 4. 确保至少 256 token
return max(max_response, 256)
这种动态限制确保了多轮对话不会超出模型的上下文窗口。
3. Prompt 工程:ContextBuilder 源码解析
如果说 LiteLLMProvider 解决了"怎么调用"的问题,那么 ContextBuilder 解决的就是"说什么"的问题------如何把各种信息组装成高效的提示词。
3.1 上下文构建流程
ContextBuilder 的 build() 方法负责组装发给 LLM 的 messages 数组:
python
# agent/context.py
class ContextBuilder:
def __init__(self, config):
self.config = config
self.memory_store = MemoryStore(config.memory_path)
self.skills_loader = SkillsLoader(config.skills_path)
async def build(self, message: Message) -> List[dict]:
messages = []
# 1. 系统提示:人格设定 + 基础信息
system_prompt = await self._build_system_prompt()
messages.append({"role": "system", "content": system_prompt})
# 2. 长期记忆(从 MEMORY.md 读取)
memory = await self.memory_store.get_long_term()
if memory:
messages.append({
"role": "system",
"content": f"【长期记忆】\n{memory}"
})
# 3. 技能摘要(可用工具描述)
skills_summary = await self.skills_loader.get_summary()
if skills_summary:
messages.append({
"role": "system",
"content": f"【可用技能】\n{skills_summary}"
})
# 4. 对话历史(短期记忆)
history = await self.session_manager.get_history(message.session_id)
messages.extend(history[-self.config.max_history:]) # 只保留最近 N 条
# 5. 当前用户消息
messages.append({"role": "user", "content": message.content})
return messages
3.2 系统提示的构建
系统提示是 LLM 行为的核心指南。nanobot 的系统提示包含多个来源:
python
async def _build_system_prompt(self):
"""构建系统提示"""
parts = []
# 1. 核心身份(从 AGENTS.md 读取)
identity = await self._read_bootstrap("AGENTS.md")
if identity:
parts.append(identity)
else:
parts.append("你是一个有用的 AI 助手。")
# 2. 当前环境信息
env_info = self._get_env_info()
parts.append(f"当前时间:{env_info.time}")
parts.append(f"工作目录:{env_info.cwd}")
# 3. 用户偏好(从 USER.md 读取)
user_prefs = await self._read_bootstrap("USER.md")
if user_prefs:
parts.append(f"用户偏好:{user_prefs}")
# 4. 工具使用指南
tool_guide = self._get_tool_usage_guide()
parts.append(tool_guide)
return "\n\n".join(parts)
这种分层构建的好处是:所有"人格""记忆""技能"都是可读的 Markdown 文件,调试和定制非常直观。
3.3 两层记忆系统
nanobot 的记忆系统非常"工程化"又易于理解:
MEMORY.md:长期记忆,存放重要事实、用户偏好、项目背景等memory/YYYY-MM-DD.md:每日笔记,自动按日期组织
MemoryStore 提供以下方法:
python
class MemoryStore:
async def get_long_term(self) -> str:
"""读取 MEMORY.md 中的长期记忆"""
path = self.workspace / "MEMORY.md"
if path.exists():
return await path.read_text()
return ""
async def get_recent_notes(self, days=7) -> str:
"""获取最近 N 天的笔记"""
notes = []
for i in range(days):
date = datetime.now() - timedelta(days=i)
note_path = self.memory_dir / f"{date:%Y-%m-%d}.md"
if note_path.exists():
notes.append(await note_path.read_text())
return "\n\n".join(notes)
async def append_today(self, content: str):
"""追加到今天的日记"""
today = datetime.now().strftime("%Y-%m-%d")
path = self.memory_dir / f"{today}.md"
async with aiofiles.open(path, "a") as f:
await f.write(f"\n\n{content}")
这种设计的巧妙之处在于:你告诉 nanobot "我的项目代号是 X",它可以在合适的时候自动写入 MEMORY.md,后续对话随时引用。
3.4 技能系统:Skills 与 Tools 的配合
nanobot 区分了两个概念:
- Skills(技能):指导文档(Markdown 文件),描述如何使用特定工具或执行特定任务
- Tools(工具):执行引擎(Python 代码),实际执行操作
在上下文构建时,SkillsLoader 会先提供技能摘要:
python
async def get_summary(self) -> str:
"""返回所有可用技能的摘要"""
summaries = []
for skill in self.skills:
if skill.always_include:
# 始终加载的技能,返回完整内容
summaries.append(await skill.get_content())
else:
# 按需加载的技能,只返回描述
summaries.append(f"- {skill.name}: {skill.description}")
return "可用技能列表:\n" + "\n".join(summaries)
当 LLM 决定使用某个技能时,它会通过 read_file 工具读取对应的 SKILL.md 文件,获取详细指导,然后调用相应的 Tools 执行操作。
3.5 动态警告系统
为了引导 Agent 高效完成任务,nanobot 会在工具执行结果中注入动态警告:
python
def _inject_warnings(self, tool_result, context):
"""根据剩余资源注入警告信息"""
warnings = []
if context.remaining_tokens < 1500:
warnings.append("⚠️ Token 即将耗尽!请优先解决最关键的问题。")
elif context.remaining_tokens < 3000:
warnings.append("⚠️ Token 不足,请聚焦主要问题。")
if context.remaining_tool_calls == 1:
warnings.append("⚠️ 最后一次工具调用机会,请谨慎使用。")
elif context.remaining_tool_calls < 5:
warnings.append(f"⚠️ 剩余 {context.remaining_tool_calls} 次工具调用。")
if warnings:
tool_result += "\n\n" + "\n".join(warnings)
return tool_result
这种设计让 Agent 能够感知资源限制,自动调整行为策略。
4. 完整调用链路:从用户消息到 LLM 响应
现在,让我们把前面所有的模块串联起来,看一条用户消息是如何经过层层处理到达 LLM 的:
实际模型 LiteLLMProvider SkillsLoader MemoryStore ContextBuilder AgentLoop 用户 实际模型 LiteLLMProvider SkillsLoader MemoryStore ContextBuilder AgentLoop 用户 alt [有工具调用] [直接回答] 发送消息 调用 build(message) 读取长期记忆 返回 MEMORY.md 获取技能摘要 返回技能列表 组装系统提示 添加对话历史 返回 messages 数组 complete(messages, tools) 懒加载 LiteLLM 计算 token 预算 添加提供商特定参数 调用实际 API 返回原始响应 返回标准化响应 执行工具 再次调用(带工具结果) 返回最终响应
5. 小结:LLM 模块的设计智慧
回顾整个 LLM 相关模块的实现,我们可以总结出几个关键的设计智慧:
| 设计要点 | 解决的问题 | 实现方式 |
|---|---|---|
| 懒加载 | 避免启动卡顿 | importlib 延迟导入 LiteLLM |
| 统一接口 | 屏蔽多提供商差异 | complete() 方法 + 参数适配 |
| 动态 token 预算 | 防止上下文溢出 | 实时计算 + 10% 安全缓冲 |
| 分层上下文 | 构建高质量提示 | 系统提示 + 记忆 + 历史 + 当前消息 |
| 文件化记忆 | 可读可编辑的持久化 | MEMORY.md + 每日笔记 |
| 技能摘要 | 避免提示词过长 | 先摘要,按需加载完整技能 |
| 动态警告 | 引导 Agent 高效行为 | 在工具结果中注入资源提示 |
正是这些设计,让 nanobot 能够在 4000 行代码内实现对 20+ 种 LLM 的灵活支持,同时保证了提示词的质量和 token 的高效利用。
下篇预告
在下一篇文章中,我们将探讨 nanobot 的记忆系统------这是 Agent 保持长期对话连贯性的关键。你将看到:
- 短期记忆如何管理(滑动窗口)
- 长期记忆如何写入和检索
- 向量数据库的集成方式(如果支持)
- 如何通过文件系统实现"永不遗忘"的记忆
敬请期待:《记忆的奥秘 ------ 对话上下文与持久化存储》
本文基于 nanobot v0.1.3 版本撰写,实际代码可能随项目迭代有所变化,建议结合最新源码阅读。