第五篇:与 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 版本撰写,实际代码可能随项目迭代有所变化,建议结合最新源码阅读。

相关推荐
Bruce_Liuxiaowei4 小时前
Prompt注入_我的AI编码助手被策反了
人工智能·ai·prompt·提示词·智能体
Sirius Wu7 小时前
意图&实体ToolCall_Prompt调优
人工智能·机器学习·语言模型·prompt·aigc
水木流年追梦8 小时前
大模型入门-大模型优化方法12-YaRN 长文本外推技术
人工智能·分布式·算法·正则表达式·prompt
kishu_iOS&AI9 小时前
LLM —— Prompt提示词工程
人工智能·prompt
龙骑士baby12 小时前
重建 AI 认知第 4 篇:Skill——提示词的系统化封装
ai·大模型·llm·prompt·skill
水木流年追梦13 小时前
大模型入门-大模型优化方法13- MTP 多 token 输出、DCA 双块注意力
人工智能·分布式·算法·正则表达式·prompt
城事漫游Molly15 小时前
AI赋能质性研究(六):跨案例比较分析,5个高质量 Prompt让AI帮你找模式
大数据·人工智能·prompt·ai for science·定性研究
超无穹科技15 小时前
# 从小说到九列分镜表:我用DeepSeek搭了一个自动分镜工具(附完整Prompt)
prompt
ABCDEEE717 小时前
API、CLI、Prompt、MCP、Skill、Agent
prompt
镜舟科技17 小时前
从 Prompt 到 Context Engineering:如何用 StarRocks 构建 AI Agent 的实时上下文引擎?
starrocks·大模型·prompt·ai agent·数据基础设施·上下文工程