构建AI个人档案:实现模型解耦与数据主权的实践方案
在实际 AI 应用开发和学习中,一个普遍且令人沮丧的现象是:你精心调教好的 AI 助手,无论是基于 ChatGPT、Claude 还是 Gemini 的 API 构建,都可能因为模型版本更新、服务商政策调整、API 接口变更甚至区域限制而突然失效。你积累的提示词工程、对话历史、个性化配置,都可能随着一个模型的“过时”或“不可用”而需要从头再来。这种对单一模型或服务的强依赖,使得个人或小型团队构建的 AI 应用缺乏长期稳定性和数据主权。
本文旨在解决这一痛点,提出并实践一套“AI 个人档案”的构建思路与实现方案。这套方案的核心思想是将你的 AI 交互逻辑、知识库、对话风格与具体的模型提供商解耦。通过一套标准化的本地配置与数据层,你可以自由地在 Claude、ChatGPT、Gemini 乃至本地部署的 Ollama 模型之间切换,而你的“档案”——即你的使用习惯和知识沉淀——保持不变。无论主流模型如何迭代、区域限制如何变化,你都能快速将你的智能体迁移到另一个可用的“大脑”上,实现真正的“一次构建,处处运行”。
我们将从概念设计开始,逐步完成一个可运行的原型系统。这套系统将涵盖配置管理、模型路由、对话持久化、上下文构建等核心模块,并最终通过一个简单的命令行或 Web 界面进行验证。文章面向有一定 Python 基础,希望构建稳定、可移植个人 AI 助手的开发者。
1. 理解“AI 个人档案”的核心:解耦、标准化与持久化
在深入代码之前,必须厘清“AI 个人档案”究竟是什么,以及它如何解决模型过时或不可用的问题。这并非一个现成的软件,而是一套设计模式和实现规范。
1.1 为什么模型会“过时”或“不可用”
从技术层面看,依赖单一远程 AI 模型服务面临多重风险:
- 服务终止与变更:服务商可能停止旧模型服务(如 GPT-3 系列),或更改 API 路径、参数格式。
- 区域与政策限制:某些模型(如 Claude, Gemini)在特定地区不可用,或对新增用户关闭注册。
- 成本与配额波动:API 定价调整、免费额度变化可能迫使你更换模型。
- 功能差异:不同模型的上下文长度、函数调用能力、输出格式支持度不同,但你的应用逻辑可能被某个模型的特性“绑定”。
当这些情况发生时,如果你的应用代码里硬编码了某个模型的 API 调用,迁移成本会非常高。
1.2 “档案”包含哪些元素
你的“AI 个人档案”应该包含所有独立于具体模型的个性化数据和配置:
- 核心配置:你的对话风格(如“扮演一个资深的软件架构师”)、温度(Temperature)、最大输出令牌数等通用参数。
- 系统提示词:定义 AI 角色、行为准则和知识范围的初始指令。这是档案的灵魂。
- 对话历史:结构化的聊天记录,包含用户消息、AI 回复、时间戳、可能的元数据(如本次对话使用的模型)。
- 知识库片段:你经常引用的文档、代码片段、个人笔记的向量化索引或简单引用。
- 工具/函数描述:如果你使用 Function Calling 或 Tool Use,对工具的定义也应标准化,以便适配不同模型的调用格式。
1.3 解耦的关键:抽象层与适配器模式
实现解耦的核心是引入一个抽象层。你的应用不直接调用openai.ChatCompletion.create或anthropic.Anthropic.messages.create,而是调用一个你自己定义的AIClient接口。这个接口背后,针对不同的模型提供商(OpenAI, Anthropic, Google Gemini, 本地 Ollama)实现具体的适配器。
你的应用代码 -> [抽象接口 AIClient] -> [OpenAI 适配器] -> OpenAI API -> [Claude 适配器] -> Anthropic API -> [Gemini 适配器] -> Google AI API -> [Ollama 适配器] -> 本地 Ollama 服务这样,当需要切换模型时,你只需在配置文件中指定另一个适配器,并确保该适配器能处理你的“档案”数据格式即可。应用的核心逻辑无需改动。
2. 环境准备与项目结构
我们将使用 Python 作为实现语言,因为它拥有最丰富的 AI 模型 SDK 生态。这个项目可以在任何能运行 Python 的环境中进行。
2.1 基础环境与依赖
首先,确保你的 Python 版本在 3.8 以上。然后,创建一个新的项目目录并初始化虚拟环境。
mkdir ai-personal-archive && cd ai-personal-archive python -m venv venv # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate接下来,创建requirements.txt文件,列出核心依赖。我们不会一次性安装所有模型的 SDK,而是按需安装。
# 核心框架与工具 langchain-core==0.3.17 langchain-community==0.3.17 pydantic>=2.0 python-dotenv>=1.0.0 # 数据持久化(选用 SQLite 和 JSON) sqlalchemy>=2.0.0 # 可选:向量数据库(用于知识库),这里先用简单的 chromadb>=0.4.22 # 模型提供商 SDK (按需安装) openai>=1.0.0 anthropic>=0.25.0 google-generativeai>=0.3.0 # Ollama 通常通过其 REST API 调用,可以用 requests,但社区有封装 ollama>=0.3.0使用 pip 安装依赖。初次可以只安装核心部分。
pip install -r requirements.txt注意:
langchain及其生态库在这里并非必需,但它提供了优秀的抽象和集成,可以极大简化我们的工作。我们主要利用其ChatModel抽象和Message数据结构。你也可以选择完全自己实现适配器。
2.2 项目结构设计
一个清晰的项目结构是维护性的基础。建议如下:
ai-personal-archive/ ├── config/ │ ├── __init__.py │ ├── settings.py # 主配置,从环境变量和文件读取 │ └── profiles/ # 存放不同的个人档案配置 │ ├── default.yaml │ └── software_architect.yaml ├── core/ │ ├── __init__.py │ ├── client.py # 抽象接口 AIClient 定义 │ ├── adapters/ # 各模型适配器 │ │ ├── __init__.py │ │ ├── base.py │ │ ├── openai_adapter.py │ │ ├── claude_adapter.py │ │ ├── gemini_adapter.py │ │ └── ollama_adapter.py │ ├── models.py # Pydantic 数据模型,如 Message, Conversation │ └── repository.py # 数据持久化层,处理对话历史存储 ├── storage/ │ ├── database.db # SQLite 数据库文件(.gitignore) │ └── knowledge/ # 知识库文档和向量存储 ├── scripts/ │ └── init_db.py # 初始化数据库的脚本 ├── .env.example # 环境变量模板 ├── .env # 本地环境变量(.gitignore) ├── requirements.txt ├── main.py # 主程序入口,可以是 CLI 或简单 Web └── README.md这个结构将配置、核心逻辑、数据存储分离,便于扩展和维护。
2.3 关键配置文件说明
在config/settings.py中,我们使用 Pydantic 来管理配置,优先从环境变量读取,便于部署。
# config/settings.py from pydantic_settings import BaseSettings from typing import Optional, Literal class Settings(BaseSettings): # 当前激活的档案名称 ACTIVE_PROFILE: str = "default" # 当前默认使用的模型提供商 DEFAULT_PROVIDER: Literal["openai", "claude", "gemini", "ollama"] = "openai" # 各 API 密钥和基础 URL (从 .env 读取) OPENAI_API_KEY: Optional[str] = None ANTHROPIC_API_KEY: Optional[str] = None GOOGLE_API_KEY: Optional[str] = None OLLAMA_BASE_URL: str = "http://localhost:11434" # 数据库路径 DATABASE_URL: str = "sqlite:///./storage/database.db" # 向量数据库路径 VECTOR_DB_PATH: str = "./storage/knowledge/chroma" class Config: env_file = ".env" extra = "ignore" # 忽略未定义的额外环境变量 settings = Settings()对应的.env文件模板.env.example内容如下:
# .env.example ACTIVE_PROFILE=default DEFAULT_PROVIDER=openai OPENAI_API_KEY=your_openai_api_key_here ANTHROPIC_API_KEY=your_anthropic_api_key_here GOOGLE_API_KEY=your_google_api_key_here # OLLAMA_BASE_URL 已有默认值,如需修改可覆盖 # DATABASE_URL 已有默认值重要:务必把.env文件加入.gitignore,避免密钥泄露。
3. 实现核心数据模型与抽象接口
有了项目骨架,我们开始实现最核心的部分:定义对话的数据结构和所有模型适配器都要遵守的接口。
3.1 定义标准化的消息与会话模型
在core/models.py中,我们使用 Pydantic 定义清晰的数据结构。
# core/models.py from pydantic import BaseModel, Field from datetime import datetime from typing import Literal, Optional, List, Dict, Any from enum import Enum class MessageRole(str, Enum): USER = "user" ASSISTANT = "assistant" SYSTEM = "system" TOOL = "tool" # 用于函数调用结果 class Message(BaseModel): """一条标准化的消息""" role: MessageRole content: str name: Optional[str] = None # 可选,参与对话的实体名称 timestamp: datetime = Field(default_factory=datetime.now) # 元数据,例如本次回复使用的具体模型名称 metadata: Dict[str, Any] = Field(default_factory=dict) class Conversation(BaseModel): """一次完整的对话会话""" id: Optional[str] = None # 数据库主键 profile_name: str # 使用的档案名称 title: Optional[str] = None # 对话标题,可由 AI 或用户生成 messages: List[Message] = Field(default_factory=list) created_at: datetime = Field(default_factory=datetime.now) updated_at: datetime = Field(default_factory=datetime.now) # 本次对话主要使用的模型提供商 provider: Literal["openai", "claude", "gemini", "ollama", "custom"] model: str # 具体模型标识,如 "gpt-4o", "claude-3-opus-20240229" def add_message(self, role: MessageRole, content: str, **kwargs): self.messages.append(Message(role=role, content=content, **kwargs)) self.updated_at = datetime.now()3.2 创建抽象客户端接口
在core/client.py中,我们定义所有适配器都必须实现的接口。
# core/client.py from abc import ABC, abstractmethod from typing import List, Optional, Dict, Any from core.models import Message, MessageRole, Conversation class AIClient(ABC): """AI 客户端抽象接口。所有模型适配器必须实现此接口。""" def __init__(self, model: str, **kwargs): self.model = model self.config = kwargs # 存放温度、最大令牌数等通用配置 @abstractmethod async def chat_completion( self, messages: List[Message], stream: bool = False, **kwargs ) -> Any: """ 核心聊天补全方法。 :param messages: 标准化的消息列表 :param stream: 是否使用流式输出 :return: 适配器原生的响应对象,或一个异步生成器(如果 stream=True) """ pass @abstractmethod def format_messages(self, messages: List[Message]) -> Any: """ 将标准化的 Message 列表转换为特定模型 API 所需的格式。 例如,OpenAI 需要 [{"role": "user", "content": "..."}], 而 Claude 可能需要不同的结构。 """ pass @abstractmethod def parse_response(self, response: Any) -> str: """ 从特定模型的响应对象中解析出纯文本回复内容。 """ pass # 可选:工具调用(Function Calling/Tool Use)的标准化方法 # @abstractmethod # async def chat_with_tools(...): # pass这个接口确保了无论底层是哪个模型,上层应用都可以用统一的方式发起对话请求和处理响应。
4. 实现具体模型适配器
现在,我们为不同的模型提供商实现具体的适配器。所有适配器放在core/adapters/目录下。
4.1 基础适配器与 OpenAI 适配器示例
首先,创建一个基础适配器,处理一些通用逻辑。
# core/adapters/base.py from core.client import AIClient from core.models import Message, MessageRole from typing import List, Any import logging logger = logging.getLogger(__name__) class BaseAdapter(AIClient): """所有适配器的基类,提供一些通用方法或默认实现。""" def __init__(self, model: str, **kwargs): super().__init__(model, **kwargs) # 可以在这里初始化 SDK 客户端,如 openai.Client self.client = None def _apply_common_params(self, params: dict) -> dict: """应用温度、最大令牌数等通用参数到请求参数字典。""" if 'temperature' in self.config: params['temperature'] = self.config['temperature'] if 'max_tokens' in self.config: params['max_tokens'] = self.config['max_tokens'] return params接下来,实现 OpenAI 适配器。你需要先安装openai库。
# core/adapters/openai_adapter.py import openai from typing import List, Any, AsyncGenerator from core.adapters.base import BaseAdapter from core.models import Message, MessageRole import logging logger = logging.getLogger(__name__) class OpenAIAdapter(BaseAdapter): def __init__(self, model: str, api_key: str, base_url: str = None, **kwargs): super().__init__(model, **kwargs) # 初始化 OpenAI 客户端 self.client = openai.OpenAI(api_key=api_key, base_url=base_url) def format_messages(self, messages: List[Message]) -> List[dict]: """将标准 Message 列表转换为 OpenAI API 格式。""" formatted = [] for msg in messages: # OpenAI 消息格式:{"role": "user", "content": "..."} formatted.append({ "role": msg.role.value, # 使用 Enum 的 value "content": msg.content }) return formatted async def chat_completion( self, messages: List[Message], stream: bool = False, **kwargs ) -> Any: formatted_messages = self.format_messages(messages) params = { "model": self.model, "messages": formatted_messages, } params = self._apply_common_params(params) params.update(kwargs) # 允许覆盖或添加其他参数 try: if stream: # 流式响应返回一个异步生成器 response = self.client.chat.completions.create(**params, stream=True) return response else: response = self.client.chat.completions.create(**params, stream=False) return response except openai.APIError as e: logger.error(f"OpenAI API 调用失败: {e}") raise def parse_response(self, response: Any) -> str: """从 OpenAI 响应中解析文本内容。""" if hasattr(response, 'choices') and len(response.choices) > 0: return response.choices[0].message.content elif hasattr(response, 'content'): # 处理流式响应中的 chunk return response.content else: # 尝试通用解析 return str(response)4.2 Claude 适配器实现要点
Claude 适配器的结构与 OpenAI 类似,但需注意 Anthropic SDK 的差异(例如,消息格式和流式响应处理)。
# core/adapters/claude_adapter.py import anthropic from typing import List, Any, AsyncGenerator from core.adapters.base import BaseAdapter from core.models import Message, MessageRole import logging logger = logging.getLogger(__name__) class ClaudeAdapter(BaseAdapter): def __init__(self, model: str, api_key: str, **kwargs): super().__init__(model, **kwargs) self.client = anthropic.Anthropic(api_key=api_key) def format_messages(self, messages: List[Message]) -> List[dict]: """将标准 Message 列表转换为 Claude API 格式。""" formatted = [] system_prompt = None # Claude 需要单独提取 system 消息 for msg in messages: if msg.role == MessageRole.SYSTEM: system_prompt = msg.content else: formatted.append({ "role": msg.role.value, "content": msg.content }) return formatted, system_prompt # 返回元组 async def chat_completion(self, messages: List[Message], stream: bool = False, **kwargs): formatted_messages, system_prompt = self.format_messages(messages) params = { "model": self.model, "messages": formatted_messages, "max_tokens": self.config.get('max_tokens', 4096), # Claude 有默认值要求 } if system_prompt: params["system"] = system_prompt params = self._apply_common_params(params) params.update(kwargs) try: if stream: with self.client.messages.stream(**params) as stream_obj: # 这里需要处理流式响应,可能返回一个自定义的生成器包装器 async for chunk in stream_obj.text_stream: yield chunk else: response = self.client.messages.create(**params) return response except anthropic.APIError as e: logger.error(f"Claude API 调用失败: {e}") raise def parse_response(self, response: Any) -> str: if hasattr(response, 'content') and len(response.content) > 0: # Claude 的 content 是一个列表,每个元素是一个 TextBlock 或 ToolUseBlock for block in response.content: if block.type == 'text': return block.text # 对于流式响应,parse_response 可能不直接调用,内容在迭代时已获取 return ""4.3 适配器工厂与配置加载
为了便于根据配置动态创建适配器,我们创建一个工厂类。
# core/adapters/__init__.py from core.adapters.openai_adapter import OpenAIAdapter from core.adapters.claude_adapter import ClaudeAdapter from core.adapters.gemini_adapter import GeminiAdapter # 需实现 from core.adapters.ollama_adapter import OllamaAdapter # 需实现 from config.settings import settings import logging logger = logging.getLogger(__name__) class AdapterFactory: _provider_map = { "openai": OpenAIAdapter, "claude": ClaudeAdapter, "gemini": GeminiAdapter, "ollama": OllamaAdapter, } @staticmethod def create_adapter(provider: str, model: str, **kwargs): """根据提供商名称创建对应的适配器实例。""" adapter_class = AdapterFactory._provider_map.get(provider) if not adapter_class: raise ValueError(f"不支持的 AI 提供商: {provider}") # 根据提供商注入必要的配置,如 API Key provider_config = {} if provider == "openai": if not settings.OPENAI_API_KEY: raise ValueError("OpenAI API Key 未配置。请在 .env 中设置 OPENAI_API_KEY") provider_config['api_key'] = settings.OPENAI_API_KEY elif provider == "claude": if not settings.ANTHROPIC_API_KEY: raise ValueError("Anthropic API Key 未配置。请在 .env 中设置 ANTHROPIC_API_KEY") provider_config['api_key'] = settings.ANTHROPIC_API_KEY elif provider == "gemini": if not settings.GOOGLE_API_KEY: raise ValueError("Google API Key 未配置。请在 .env 中设置 GOOGLE_API_KEY") provider_config['api_key'] = settings.GOOGLE_API_KEY elif provider == "ollama": provider_config['base_url'] = settings.OLLAMA_BASE_URL # 合并通用配置和提供商特定配置 all_config = {**provider_config, **kwargs} return adapter_class(model=model, **all_config)5. 构建对话管理与持久化层
适配器解决了“怎么问”的问题,接下来需要解决“问什么”和“记住对话”的问题。这由档案配置和持久化层负责。
5.1 定义个人档案配置
在config/profiles/default.yaml中,定义一个档案:
# config/profiles/default.yaml name: "default" description: "通用助手档案" system_prompt: | 你是一个乐于助人、知识渊博的 AI 助手。请用清晰、有条理的方式回答用户的问题。 如果遇到不确定的信息,请诚实说明。 请使用中文进行交流。 provider: "openai" # 默认提供商 model: "gpt-4o-mini" # 默认模型 parameters: temperature: 0.7 max_tokens: 2000 # 可以定义工具列表(未来扩展) # tools: [] # 可以关联知识库(未来扩展) # knowledge_base: "default_kb"5.2 实现对话仓库(Repository)
在core/repository.py中,我们使用 SQLAlchemy 来存储和读取对话。
# core/repository.py from sqlalchemy import create_engine, Column, String, DateTime, Text, JSON from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime from typing import List, Optional import json from core.models import Conversation, Message, MessageRole from config.settings import settings Base = declarative_base() class DBConversation(Base): __tablename__ = 'conversations' id = Column(String, primary_key=True) profile_name = Column(String, nullable=False) title = Column(String) messages = Column(Text) # 存储为 JSON 字符串 provider = Column(String) model = Column(String) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) class ConversationRepository: def __init__(self, database_url: str = None): db_url = database_url or settings.DATABASE_URL self.engine = create_engine(db_url, connect_args={"check_same_thread": False} if "sqlite" in db_url else {}) Base.metadata.create_all(bind=self.engine) self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) def save_conversation(self, conversation: Conversation) -> str: """保存或更新一个对话会话。""" db_session = self.SessionLocal() try: # 将 messages 列表序列化为 JSON 字符串 messages_json = json.dumps([msg.dict() for msg in conversation.messages], ensure_ascii=False) db_conv = DBConversation( id=conversation.id or str(uuid.uuid4()), profile_name=conversation.profile_name, title=conversation.title, messages=messages_json, provider=conversation.provider, model=conversation.model, created_at=conversation.created_at, updated_at=conversation.updated_at ) db_session.merge(db_conv) # 使用 merge 处理更新 db_session.commit() return db_conv.id finally: db_session.close() def load_conversation(self, conversation_id: str) -> Optional[Conversation]: """根据 ID 加载一个对话会话。""" db_session = self.SessionLocal() try: db_conv = db_session.query(DBConversation).filter(DBConversation.id == conversation_id).first() if not db_conv: return None # 将 JSON 字符串反序列化为 Message 对象列表 messages_data = json.loads(db_conv.messages) messages = [Message(**data) for data in messages_data] return Conversation( id=db_conv.id, profile_name=db_conv.profile_name, title=db_conv.title, messages=messages, provider=db_conv.provider, model=db_conv.model, created_at=db_conv.created_at, updated_at=db_conv.updated_at ) finally: db_session.close() def list_conversations(self, profile_name: str = None, limit: int = 50) -> List[Conversation]: """列出对话会话,可按档案过滤。""" db_session = self.SessionLocal() try: query = db_session.query(DBConversation) if profile_name: query = query.filter(DBConversation.profile_name == profile_name) query = query.order_by(DBConversation.updated_at.desc()).limit(limit) db_convs = query.all() conversations = [] for db_conv in db_convs: messages_data = json.loads(db_conv.messages) messages = [Message(**data) for data in messages_data] conversations.append(Conversation( id=db_conv.id, profile_name=db_conv.profile_name, title=db_conv.title, messages=messages, provider=db_conv.provider, model=db_conv.model, created_at=db_conv.created_at, updated_at=db_conv.updated_at )) return conversations finally: db_session.close()5.3 创建数据库初始化脚本
运行scripts/init_db.py来创建数据库表。
# scripts/init_db.py from core.repository import Base, engine import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def init_db(): try: Base.metadata.create_all(bind=engine) logger.info("数据库表创建成功。") except Exception as e: logger.error(f"数据库初始化失败: {e}") if __name__ == "__main__": init_db()执行命令:
python scripts/init_db.py6. 组装与运行:创建一个简单的命令行聊天程序
现在,我们将所有组件组装起来,创建一个简单的命令行交互程序,验证整个流程。
6.1 主程序逻辑
在main.py中,我们实现一个循环,读取用户输入,调用相应的适配器,并保存对话。
# main.py import asyncio import yaml import os from typing import Optional from core.adapters import AdapterFactory from core.repository import ConversationRepository from core.models import Conversation, Message, MessageRole from config.settings import settings class ChatSession: def __init__(self, profile_name: str = None): self.profile_name = profile_name or settings.ACTIVE_PROFILE self.profile = self._load_profile(self.profile_name) self.conversation = Conversation( profile_name=self.profile_name, provider=self.profile['provider'], model=self.profile['model'], title="未命名对话" ) self.repo = ConversationRepository() # 初始化适配器 self.adapter = AdapterFactory.create_adapter( provider=self.profile['provider'], model=self.profile['model'], **self.profile.get('parameters', {}) ) # 添加系统提示词(如果存在) if 'system_prompt' in self.profile and self.profile['system_prompt']: self.conversation.add_message(MessageRole.SYSTEM, self.profile['system_prompt']) def _load_profile(self, name: str) -> dict: profile_path = os.path.join('config', 'profiles', f'{name}.yaml') if not os.path.exists(profile_path): raise FileNotFoundError(f"档案配置文件未找到: {profile_path}") with open(profile_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) async def chat_loop(self): print(f"\n=== 启动聊天会话 ===") print(f"档案: {self.profile_name}") print(f"模型: {self.profile['provider']} - {self.profile['model']}") print("输入 '/exit' 退出, '/save' 保存对话, '/load <id>' 加载历史对话") print("="*30) while True: try: user_input = input("\nYou: ").strip() if not user_input: continue if user_input.lower() == '/exit': print("退出聊天。") break if user_input.lower() == '/save': conv_id = self.repo.save_conversation(self.conversation) print(f"对话已保存,ID: {conv_id}") continue if user_input.startswith('/load'): parts = user_input.split() if len(parts) == 2: conv_id = parts[1] loaded = self.repo.load_conversation(conv_id) if loaded: self.conversation = loaded # 需要根据加载的 conversation 重新创建适配器吗? # 这里简化处理,使用加载的 provider/model,但 adapter 参数可能不同。 # 更好的做法是重建 adapter。 self.adapter = AdapterFactory.create_adapter( provider=loaded.provider, model=loaded.model, **self.profile.get('parameters', {}) ) print(f"已加载对话: {loaded.title}") # 打印最后几条消息 for msg in loaded.messages[-5:]: print(f"{msg.role.value}: {msg.content[:100]}...") else: print(f"未找到对话 ID: {conv_id}") continue # 1. 将用户输入添加到 conversation self.conversation.add_message(MessageRole.USER, user_input) # 2. 调用 AI 适配器 print("AI: ", end='', flush=True) full_response = "" # 这里使用非流式简化演示,实际可以使用流式 response = await self.adapter.chat_completion( messages=self.conversation.messages, stream=False ) ai_response_text = self.adapter.parse_response(response) print(ai_response_text) # 3. 将 AI 回复添加到 conversation self.conversation.add_message(MessageRole.ASSISTANT, ai_response_text) except KeyboardInterrupt: print("\n\n会话被中断。") break except Exception as e: print(f"\n发生错误: {e}") # 可以选择记录日志,这里简单打印 async def main(): session = ChatSession() await session.chat_loop() if __name__ == "__main__": asyncio.run(main())6.2 运行与验证
准备环境变量:复制
.env.example为.env,并填入你至少一个可用的 API Key(例如 OpenAI)。cp .env.example .env # 编辑 .env 文件,填入 OPENAI_API_KEY=sk-...准备档案配置:确保
config/profiles/default.yaml存在,并且其中的model是你有权限访问的(例如gpt-3.5-turbo)。运行程序:
python main.py验证功能:
- 程序启动后,应显示使用的档案和模型。
- 输入普通问题,如“你好,请介绍你自己”,应能收到 AI 回复。
- 输入
/save,控制台应输出一个 UUID,表示对话已保存到数据库。 - 输入
/load <刚才的UUID>,应能加载历史对话并显示最后几条消息。 - 输入
/exit退出。
至此,一个具备核心功能的“AI 个人档案”系统原型就完成了。你可以通过切换.env中的DEFAULT_PROVIDER或创建新的档案配置文件,轻松更换背后的 AI 模型。
7. 常见问题排查与优化建议
在实际使用和扩展此系统时,你可能会遇到以下问题。
7.1 配置与连接问题
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
程序启动时报ValueError: OpenAI API Key 未配置 | .env文件不存在、路径错误或 KEY 未填写。 | 1. 确认项目根目录下存在.env文件。2. 检查 .env文件中OPENAI_API_KEY等变量名是否正确,值是否已填写。3. 在 config/settings.py中打印settings对象,查看配置是否加载。 | 确保.env文件格式正确,变量名与settings.py中定义的完全一致。 |
调用 API 时出现AuthenticationError或401 | API Key 无效、过期或没有对应模型的权限。 | 1. 前往对应平台(如 OpenAI 控制台)检查 API Key 状态和余额。 2. 确认使用的模型名称(如 gpt-4)在你的账户中可用。 | 更换有效的 API Key,或在平台申请相应模型的访问权限。 |
报错Claude is not available in your country或Gemini 不支持你所在的地区 | 服务商的地理限制。 | 1. 确认你的 IP 地址所在地。 2. 查阅服务商官方文档的区域支持列表。 | 1. 考虑使用合规的网络服务(确保业务合法性)。 2. 切换到可用的其他模型提供商(如 OpenAI 或本地 Ollama)。这正是本系统的优势。 |
| 连接 Ollama 超时 | Ollama 服务未启动或端口不对。 | 1. 运行ollama serve确保服务在运行。2. 检查 OLLAMA_BASE_URL配置(默认http://localhost:11434)。3. 使用 curl http://localhost:11434/api/tags测试连通性。 | 启动 Ollama 服务,并确保配置的 URL 和端口正确。 |
7.2 数据与逻辑问题
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 保存对话后,重新加载发现消息丢失或乱码 | 1. 数据库字段长度限制。 2. JSON 序列化/反序列化编码问题。 | 1. 检查storage/database.db文件大小是否正常增长。2. 在 repository.py的save_conversation和load_conversation方法中添加调试日志,打印序列化前后的数据。 | 1. 将DBConversation.messages字段类型改为Text(SQLAlchemy 对应数据库的TEXT类型),它没有长度限制。2. 在 json.dumps和json.loads中明确指定ensure_ascii=False以支持中文。 |
| 切换档案后,AI 的回复风格没变 | 系统提示词(system_prompt)未正确应用到新的对话中。 | 1. 检查新档案的 YAML 文件中system_prompt字段是否正确。2. 在 ChatSession.__init__中打印加载后的profile内容。 | 确保在创建新的ChatSession或加载对话后,将档案中的system_prompt作为一条SYSTEM角色的消息插入到conversation.messages列表的开头。注意,有些模型(如 Claude)要求 system 提示词单独传递。 |
| 流式输出不工作或显示异常 | 适配器的chat_completion流式处理逻辑有误,或主程序未正确处理异步生成器。 | 1. 在适配器中,确保stream=True时返回的是一个可迭代/异步生成器对象。2. 在主程序的 chat_loop中,需要使用async for来消费流式响应。 | 参考各 SDK 官方文档的流式示例。对于异步生成器,主程序调用方式需改为:stream_response = await adapter.chat_completion(..., stream=True)async for chunk in stream_response:print(chunk, end='', flush=True) |
7.3 性能与扩展建议
- 上下文长度管理:长时间对话后,消息列表会很长,可能超过模型上下文限制。需要在
repository.py的load_conversation或调用适配器前,实现一个“上下文窗口”函数,只保留最近 N 条消息或通过总结压缩历史。 - 异步优化:当前
chat_loop是同步输入,但 API 调用是异步的。对于 Web 应用,应使用完全的异步框架(如 FastAPI + WebSockets)来处理并发请求。 - 错误处理与重试:网络请求可能失败。应在适配器的
chat_completion方法中加入重试逻辑(如使用tenacity库)和更细致的错误分类处理。 - 配置热重载:目前档案配置在会话初始化时加载。可以监听配置文件变化,实现热重载,无需重启应用。
- 知识库集成:在档案配置中增加
knowledge_base字段。在发送消息给模型前,先查询向量数据库(如 Chroma),将相关片段作为上下文插入系统提示词或用户消息中。 - 工具调用标准化:实现
AIClient接口中的chat_with_tools抽象方法,定义统一的工具描述格式,并在各适配器中转换为对应的 Function Calling 或 Tool Use 格式。
8. 总结:从原型到生产
我们构建的这套“AI 个人档案”系统原型,已经实现了最核心的价值:将你的 AI 交互配置、历史与具体的模型服务解耦。通过配置文件,你可以定义不同的助手角色;通过更换配置文件中的provider和model,你可以无缝在 ChatGPT、Claude、Gemini 和本地模型间切换;所有的对话历史都以标准化格式保存在你的本地数据库中,完全由你掌控。
要将此原型用于生产环境或更严肃的个人用途,还需要在以下几个方面加强:
- 安全性:
.env中的 API 密钥应通过更安全的方式管理(如密钥管理服务)。数据库文件应加密或放在安全位置。 - 可观测性:加入详细的日志记录(请求、响应、错误),便于排查问题。可以记录 Token 消耗,用于成本分析。
- 用户界面:将命令行程序扩展为 Web 界面(使用 Gradio、Streamlit 或前端框架),提供更好的交互体验。
- 版本管理:为对话和档案配置引入版本控制,便于回滚和对比。
- 备份与导出:提供对话历史的导出功能(如 Markdown、JSON),并实现定期自动备份。
最重要的是,这套架构赋予了你选择权。当某个模型服务变得昂贵、受限或停止服务时,你不再需要重写整个应用,只需为新的模型实现一个适配器,并更新你的配置文件。你的数字记忆和交互习惯,将不再受制于任何单一的商业实体,从而获得长久的可用性。
