第五篇:与 LLM 对话 —— 模型接口封装与 Prompt 工程

LiteLLMProviderContextBuilder,看 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+ 种)              │
└─────────────────────────────────────────────────────────────┘

核心设计理念

  1. 统一接口 :无论后端是什么模型,AgentLoop 都通过 complete(messages, tools) 这一个方法调用
  2. 配置驱动 :通过 config.json 指定使用哪个提供商、哪个模型,无需修改代码
  3. 上下文组装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:本地部署的开源模型
  • GroqZhipu

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 上下文构建流程

ContextBuilderbuild() 方法负责组装发给 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 版本撰写,实际代码可能随项目迭代有所变化,建议结合最新源码阅读。

相关推荐
XLYcmy4 小时前
智能体大赛 总结与展望 比赛总结
大数据·ai·llm·prompt·agent·qwen·万方数据库
光的方向_8 小时前
ChatGPT提示工程入门 Prompt 03-迭代式提示词开发
人工智能·chatgpt·prompt·aigc
王解9 小时前
第四篇:万能接口 —— 插件系统设计与实现
人工智能·nanobot
XLYcmy1 天前
智能体大赛 实现逻辑 大容量数据预处理机制
ai·llm·json·prompt·api·检索·万方数据库
XLYcmy1 天前
智能体大赛 实现逻辑 “检索先行”的闭环工作流
数据库·ai·llm·prompt·agent·rag·万方
AI Echoes1 天前
对接自定义向量数据库的配置与使用
数据库·人工智能·python·langchain·prompt·agent
大好人ooo1 天前
Prompt 工程基础方法介绍
prompt
王解2 天前
第一篇:初识 nanobot —— 一个微型 AI Agent 的诞生
人工智能·nanobot
XLYcmy2 天前
智能体大赛 核心功能 惊喜生成”——创新灵感的催化器
数据库·ai·llm·prompt·agent·检索·万方