Skip to content

模型与辅助系统设计

记录清海 AI 的 LLM 调用链路(模型调用现状、统一方案、模型切换策略)、记忆与知识系统(PersonContext / Memory / Knowledge / Daily Notes / context providers 的边界与协作)、以及技能系统(渐进式信息披露、常驻工具与按需技能)的完整设计。 如本文与《产品与架构设计》冲突,以后者为准。 最后更新:2026-03-18


一、设计原则

自己封装 HTTP 调用,不用官方 SDK。

原因:

  1. 上下文必须可控:清海的对话上下文组装包含固定规则、人物切片、会话简报、Concern 摘要、按需业务上下文和记忆检索等分层内容,并受可见性裁剪约束;token 预算、Pruning、Compaction 都要求精确控制每次发给模型的 messages 内容。具体分层以《对话流程设计》《权限与可见性设计》为准。官方 SDK 自己管上下文,无法介入
  2. 中转站要求特定请求头:项目通过本地代理(ANTHROPIC_BASE_URL)访问模型,中转站对 headers 有限制,需要模拟 Claude Code CLI 格式
  3. 灵活切换模型和 provider:不绑死某个 SDK,底层走标准 HTTP,换模型只改配置

二、当前现状:三条独立链路

链路一:主对话(scripts/direct/client.py

项目说明
用途所有用户对话(TG、飞书等)
DirectClaudeClient
HTTPhttpx 异步
流式SSE 流式解析(自己实现 _iter_sse()
Agent Loop有,流式响应 → tool_use → 执行工具 → 回传结果 → 继续循环
认证环境变量 ANTHROPIC_AUTH_TOKEN
端点{ANTHROPIC_BASE_URL}/v1/messages
多 provider不支持,写死环境变量
Session本地管理对话历史,持久化到 MongoDB
Hookson_user_prompt(记忆检索)、on_stop(保存对话)、on_pre/post_tool_use

调用链:

用户消息
  → avatar_core.call_claude_avatar()
    → LiveSessionManager.query()          [tg_claude_sdk.py, 桥接层]
      → DirectSessionManager.query()      [direct/session.py]
        → DirectClaudeClient.agent_loop() [direct/client.py]
          → _stream_api()                 [httpx POST + SSE]

链路二:内部 LLM 调用(scripts/llm/client.py

项目说明
用途记忆检索打分、session brief 更新等内部调用
函数call_llm()
HTTPrequests 同步
流式非流式,等完整响应
Agent Loop
认证.config.jsonllm_providers.{name}.api_key
端点{api_base}/messages
多 provider支持,从 .config.json 读取多个 provider 配置
格式转换接受 OpenAI 格式输入,内部转 Anthropic 格式,返回 OpenAI 格式

链路三:向量嵌入(scripts/memory/embedder.py

项目说明
用途文本向量化(记忆存储、语义检索)
函数embed_text() / embed_batch()
HTTPrequests 同步
接口OpenAI 兼容 /embeddings(与对话 API 完全不同)
认证.config.jsonllm_providers.embedding.api_key
模型text-embedding-v3(通义千问),1024 维
缓存LRU 内存缓存(最多 1000 条)

现状问题

  1. 三条链路各自为政direct/client.pyllm/client.pyembedder.py 的请求头、错误处理、provider 配置各写一套
  2. 多 provider 能力不统一llm/client.py 支持多 provider,direct/client.py 不支持,embedder.py 单独配置
  3. 改一个另一个不跟:Anthropic API 更新时需要改多个地方
  4. 残留依赖pyproject.toml 里还有 claude-agent-sdk,代码已不使用

三、统一方案

目标

统一入口:不管以后接多少个模型,所有调用都通过同一个 LLMClient 类进入。对话、内部调用、向量嵌入,全部是 LLMClient(provider=xxx) 实例化后调对应方法。provider 配置统一写在 .config.jsonllm_providers,加新模型只加配置,不改代码。

所有调用方

LLMClient(provider=...)         ← 唯一入口
    ├── agent_loop()            → 主对话(async + SSE)
    ├── sync_call()             → 内部调用(sync,非流式)
    └── embed() / embed_batch() → 向量嵌入(sync,/embeddings 端点)

类名:LLMClient

放在 scripts/llm/client.pyscripts/direct/client.py 改为薄包装,re-export LLMClient as DirectClaudeClient

配置优先级

构造函数参数 > 环境变量 > .config.json provider 配置
  • LLMClient()(无 provider)→ 读环境变量 ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN,用于主对话
  • LLMClient(provider="dashscope") → 从 .config.json 读,用于内部调用
  • LLMClient(provider="embedding") → 从 .config.json 读,调 embed 方法

Headers 按 provider 区分

provider 配置新增可选字段 "api_style": "claude" | "openai"(默认 "claude"):

  • "claude":完整 Claude Code 伪装头(stainless/beta 等)+ SYSTEM_PROMPT_PREFIX
  • "openai":标准 headers(Authorization: Bearer),不加伪装头和 system prefix

无 provider 时(环境变量模式)默认 "claude" 风格。

Sync/Async 共存

一个类,两套 HTTP 方法:

  • Async(httpx):agent_loop(), query(), ask(), count_tokens(), compress_messages()
  • Sync(requests):sync_call() — 单次非流式调用

共享:_build_headers(), _build_system_blocks(), _build_body()

Token 计费

TokenUsage.calc_cost() 继续用于 SDKChunk.cost 展示。llm_usage.record_usage() 已移除(不再写计费 DB)。

LLMClient 类设计

python
class LLMClient:
    def __init__(self, model=None, max_tokens=None, max_turns=None,
                 base_url=None, auth_token=None, provider=None):
        if provider:
            config = load_config(provider)
            self.base_url = config['api_base']
            self._auth_token = config['api_key']
            self.model = model or config['model']
            self._api_style = config.get('api_style', 'claude')
            self._proxy = config.get('proxy')
            self._timeout = config.get('timeout', 30)
        else:
            self.base_url = base_url or os.environ.get("ANTHROPIC_BASE_URL", ...)
            self._auth_token = auth_token or os.environ.get("ANTHROPIC_AUTH_TOKEN", ...)
            self.model = model or DEFAULT_MODEL
            self._api_style = "claude"
            ...

    # --- 共享基础设施 ---

    def _build_headers(self): ...
    def _build_system_blocks(self, system): ...
    def _build_body(self, messages, system_blocks, tool_params, stream=True): ...

    # --- Async 方法(搬自 DirectClaudeClient,改动极小) ---

    async def agent_loop(...): ...
    async def query(...): ...
    async def ask(...): ...
    async def count_tokens(...): ...
    async def compress_messages(...): ...

    # --- Sync 方法(基于现有 call_llm 逻辑) ---

    def sync_call(self, messages, system=None, tools=None,
                  temperature=0.3, max_tokens=4096) -> dict: ...

    # --- 向量嵌入:Sync,复用认证和配置,走 /embeddings 端点 ---

    def _build_embed_headers(self) -> dict: ...  # 始终用 Authorization: Bearer
    def embed(self, text: str, dimensions=1024) -> list[float]: ...
    def embed_batch(self, texts: list[str], dimensions=1024) -> list[list[float]]: ...

兼容函数

调用方零改动,内部代理到 LLMClient

python
# llm/client.py
def call_llm(messages, tools=None, temperature=0.3, provider=None) -> dict | None:
    client = LLMClient(provider=provider or _get_default_provider())
    system, anthropic_msgs = _openai_to_anthropic(messages)
    data = client.sync_call(anthropic_msgs, system=system, ...)
    return _anthropic_to_openai(data)

# memory/embedder.py — 单例客户端 + LRU 缓存
_embed_client: LLMClient | None = None

def _get_embed_client() -> LLMClient:
    """懒加载单例,model 变更后自动重建(感知配置热重载)。"""
    global _embed_client, _embed_client_model
    cfg = load_config("embedding")
    if _embed_client is None or cfg["model"] != _embed_client_model:
        _embed_client = LLMClient(provider="embedding")
        _embed_client_model = cfg["model"]
    return _embed_client

def embed_text(text: str) -> list[float]:    # LRU 缓存 → _get_embed_client().embed()
def embed_batch(texts: list[str]) -> list[list[float]]:  # 命中缓存跳过,未命中批量一次调用

上层接口保持兼容

  • LiveSessionManager / DirectSessionManager 的接口不变
  • call_llm() 的接口不变
  • embed_text() / embed_batch() 的接口不变(内部改为代理 LLMClient
  • 只是底层实现统一

四、模型切换

对话模型:随时可切

切换方式:修改 .config.json 中 provider 的 modelapi_base,配置热重载,不用重启。

前提条件:

  • 目标模型支持 Anthropic Messages API 格式(或中转站做了格式转换)
  • 支持 tool_use(agent loop 需要)
  • 支持 streaming SSE

不同场景可以用不同模型:

  • 主对话用大模型(如 claude-sonnet-4-6)
  • 内部打分/摘要用小模型(更便宜更快)

向量模型:需谨慎

切换前提:

  • 新模型支持 OpenAI 兼容 /embeddings 接口
  • 维度对齐:新模型输出的向量维度需与数据库中已有数据一致(当前 1024 维)

重要:换向量模型意味着旧向量全部作废,需要全量重新 embed。因为不同模型的向量空间不同,混用会导致相似度计算无意义。

建议的切换流程:

  1. 新模型产出 embed,写入新 collection
  2. 验证检索质量
  3. 切换读取指向新 collection
  4. 清理旧数据

五、配置示例

.config.json 新增 api_style 字段(可选,默认 "claude"):

json
{
  "llm_providers": {
    "default": {
      "api_base": "http://127.0.0.1:18080/v1",
      "api_key": "your_key",
      "model": "claude-sonnet-4-6",
      "api_style": "claude",
      "timeout": 30,
      "proxy": null
    },
    "dashscope": {
      "api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1",
      "api_key": "sk-xxx",
      "model": "qwen-plus",
      "api_style": "openai",
      "timeout": 30
    },
    "embedding": {
      "api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1",
      "api_key": "sk-xxx",
      "model": "text-embedding-v3",
      "api_style": "openai",
      "timeout": 30
    }
  }
}

六、文件变更

1. scripts/llm/client.py — 重写(核心)

direct/client.py 搬入:

  • 常量:CLAUDE_CODE_HEADERS, SYSTEM_PROMPT_PREFIX, DEFAULT_MODEL/MAX_TOKENS/MAX_TURNS, WEB_SEARCH_TOOL
  • 数据类:HookSet, SDKChunk, ToolDef, TokenUsage
  • 工具函数:_iter_sse(), _ulog_tool()

新增 LLMClient 类(合并 DirectClaudeClient 的 async 方法 + call_llm 的 sync 方法 + embed 嵌入方法)。

保留 call_llm() 为兼容函数,保留 load_config(), get_provider_names(), reload_config(), _openai_to_anthropic() 等辅助函数。

2. scripts/direct/client.py — 改为薄包装

python
from scripts.llm.client import (
    LLMClient as DirectClaudeClient,
    HookSet, SDKChunk, ToolDef, TokenUsage,
    DEFAULT_MODEL, DEFAULT_MAX_TOKENS, DEFAULT_MAX_TURNS,
    SYSTEM_PROMPT_PREFIX, CLAUDE_CODE_HEADERS, WEB_SEARCH_TOOL,
)

所有现有 from scripts.direct.client import ...from scripts.direct import ... 路径不变。

3. scripts/llm/__init__.py — 新增导出 LLMClient

4. scripts/memory/embedder.py — 改为代理 LLMClient

移除:独立 HTTP 逻辑(_get_embed_session_get_embed_config、直接 requests.post)。

改为:

  • scripts.llm.client 导入 LLMClientMAX_EMBED_CHARSload_config
  • _get_embed_client() 懒加载单例;检测到 config 中 model 变更时自动重建
  • embed_text() / embed_batch() 保持原有签名,内部代理到 LLMClient.embed() / LLMClient.embed_batch()
  • embed_batch() 收集所有缓存未命中项后一次 API 调用批量获取("input": [list]),按 index 字段排序结果

不需要改动的文件

文件原因
scripts/direct/__init__.pyfrom .client import DirectClaudeClient 自动从薄包装获取
scripts/direct/session.py懒导入 from .client import DirectClaudeClient,类名不变
scripts/tg_claude_sdk.pyfrom scripts.direct import DirectClaudeClient,不受影响
scripts/memory/searcher.py通过 call_llm() 调用,接口不变
scripts/memory/session_brief.py通过 call_llm() 调用,接口不变
scripts/direct/hooks.py不直接引用 client
scripts/direct/tools/task.pyfrom scripts.direct.client import DirectClaudeClient,别名保持

七、执行顺序与验证

执行顺序

  1. scripts/llm/client.py — 重写:搬入常量/数据类 + 实现 LLMClient + 保留 call_llm 兼容 + 新增 embed 方法
  2. scripts/direct/client.py — 替换为薄包装
  3. scripts/llm/__init__.py — 更新导出
  4. scripts/memory/embedder.py — 改为代理 LLMClient,MAX_EMBED_CHARSllm/client 导入

验证(2026-03-12 实测)

  1. 同步链路python -c "from scripts.llm import call_llm; ..."
  2. 异步链路python scripts/direct/client.py(现有 _test 函数)✓
  3. 导入兼容python -c "from scripts.direct import DirectClaudeClient, SDKChunk, ToolDef"
  4. session 兼容python -c "from scripts.direct import DirectSessionManager"
  5. lintruff check scripts/llm/client.py scripts/direct/client.py scripts/memory/embedder.py
  6. 向量链路python -c "from scripts.memory.embedder import embed_text, embed_batch"
  7. 单元测试:7/7 通过(导入、call_llm 兼容、LLMClient 实例化、embed 方法、embed_batch、薄包装别名、embedder 代理)

八、风险点

风险应对
循环导入LLMClient 无外部内部模块依赖,原 llm_usage.record_usage() 调用已移除
httpx 依赖scripts/llm/client.py 新增 import httpx(顶层),requests 保持懒导入
proxy 配置差异无 provider 模式从 .config.jsontelegram.proxy 读取(沿用现有逻辑)
_build_body stream 参数从硬编码 True 改为参数化,sync_call 传 stream=False
embedder 单例感知配置热重载_get_embed_client() 每次调用检测 config model 字段变更,变更时自动重建客户端

九、待实现

  • [x] 实现 LLMClient 统一类(合并 direct/client.pyllm/client.py
  • [x] direct/client.py 改为薄包装
  • [x] llm/__init__.py 更新导出
  • [x] memory/embedder.py 改为代理 LLMClient(embed 链路统一入口)
  • [x] 清理 pyproject.toml 中的 claude-agent-sdk 残留依赖 — 已移除,现为 claude-code-sdk
  • [ ] 向量模型切换流程(全量重 embed + 验证)

十、记忆与知识系统

定义 PersonContext、Memory、Knowledge、Daily Notes、context providers 各自的边界与协作方式。 Memory 是长期辅助认知,不是人物、事实、议程、Concern 的运行态真源。

10.1 系统分工

当前与信息沉淀相关的能力分为五层:

作用
PersonContext当前人物工作视图
Memory历史事实、经验、规则沉淀
Knowledge文档、制度、说明、长期资料
Daily Notes每日原始工作流水
context_providers实时业务快照

其中,Agenda / Concern 属于运行态真源,不属于记忆系统。

10.2 边界说明

PersonContext

回答:

  • 这个人是谁
  • 现在在做什么
  • 当前卡在哪
  • 应该怎么和他沟通

Memory

回答:

  • 过去发生过什么
  • 某次承诺是什么
  • 某条经验和规则是什么

Knowledge

回答:

  • 文档怎么写
  • 制度和定义是什么
  • 公司长期资料有哪些

Daily Notes

回答:

  • 今天发生了什么
  • 系统今天做了什么

context_providers

回答:

  • 当前实时业务状态是什么

10.3 当前记忆写入方式

对话侧当前主要写入两类记忆:

  1. chat

    • 对话提炼后的内容
    • scope 一般按当前会话隔离
  2. rule

    • 审批经验
    • 承诺 / 规则 / 高价值结论
    • 通常进入 company 级别可复用范围

人物工作状态变化则直接写入 person_work_state,不通过记忆主库兜底。

议程变化写入 Agenda / Concern,也不通过 Memory 兜底。

10.4 检索策略

当前对话采用按需检索:

  • prompt 不整包预塞大量记忆
  • 模型需要时调用 MemorySearch
  • session_brief 负责承接短期会话状态

这使记忆更像"可调用的历史库",而不是固定附在每次对话上的长附件。

10.5 当前原则

  1. 人物当前真相在 PersonContext
  2. 历史事实与规则在 Memory
  3. 长期资料在 Knowledge
  4. 每日流水在 Daily Notes
  5. 实时业务状态在 context providers
  6. 议程与事项运行态在 Agenda / Concern

它们互补协作,但不互相替代。


十一、技能系统

记录清海如何以"轻提示 + 按需展开"的方式使用技能与工具。

11.1 核心思路

技能系统采用渐进式信息披露:

  1. system prompt 中只放技能目录摘要
  2. 模型需要时再读取对应 SKILL.md
  3. 真正执行时再调用工具

这样可以保持 prompt 干净,同时允许技能持续扩展。

11.2 三层结构

text
第一层:技能目录摘要
第二层:读取某个 SKILL.md
第三层:调用真实工具

第一层

当前通过 discover_skills() 把技能名称和路径注入 prompt。

第二层

模型自己读取某个 SKILL.md,理解使用方法、限制与最佳实践。

第三层

模型调用真实工具完成动作。

11.3 常驻工具与技能的边界

常驻工具

适合 schema 自解释、无需额外长文说明的能力:

  • MemorySearch
  • 渠道业务工具
  • concern 工具
  • update_work_status

技能

适合需要更长说明文档的能力:

  • 复杂外部系统
  • 多步骤操作规范
  • 某类特定业务 SOP

11.4 不会作为技能的能力

以下能力由系统自动提供,不通过 SKILL.md 暴露:

能力进入方式
session_brief对话自动注入
person context slice对话自动注入
concern 摘要对话自动注入
context providers对话按需注入

这些是对话运行时底座,不是"可选技能"。

11.5 设计目标

技能系统的目标只有两个:

  1. 让模型知道自己还有哪些延伸能力
  2. 不让这些说明文档持续占用主 prompt 预算

当前实现已经围绕这两个目标展开。

Boss-AGI · 超级 AI 企业助理