文章

7 · 面向对象:封装、继承与系统解耦

#007 · 2026-04-16 · Python
Reading Path / ARTICLE 先抓主张,再转成行动 #007 · Python · 读完进入产品或下一篇

🔗 知识图谱导航:阅读本文前,建议先掌握/回顾 《6 · 闭包与装饰器:函数的终极形态》 中的核心概念;本文会在这个基础上继续推进。 承上启下:上一篇我们用装饰器扩展函数行为.现在进入更大的组织单元——类。OOP 的核心价值不是"写类",而是系统解耦:让不同组件可以独立变化。

极客解析:先把数据流、控制流和模块边界跑通,再谈抽象;每段代码都围绕一个可执行 CLI 闭环展开。


痛点与架构

用大模型接口封装演示 OOP:你有 OpenAI、本地 Ollama、Mock 三种后端,上层代码不应该关心用的是哪个。

BaseAgent(抽象基类)
    ├── OpenAIAgent(子类:调用 OpenAI API)
    ├── OllamaAgent(子类:调用本地 Ollama)
    └── MockAgent  (子类:测试用 Mock)

AgentRouter(路由器)
    └── 根据配置选择 Agent,上层代码无感知
OOP 特性 体现 价值
封装 __api_key 私有,@property 暴露接口 隐藏实现细节
继承 OpenAIAgent(BaseAgent) 复用公共逻辑
多态 所有 Agent 都有 .chat() 方法 上层代码统一调用

实战演练场

步步为营:核心逻辑自适应拆解

这一篇不要求你一口气吃下面向对象。我们把它拆成“数据盒子 -> 接口合同 -> 具体子类 -> 路由分发 -> 会话管理 -> CLI 总控台”。你先跑小块,再看完整架构,就不会被类、继承、多态一起砸懵。

Step 1:用 Message 把一条聊天消息装进标准盒子

痛点与机制:

Message 是最小的数据盒子,里面固定放 role、content、timestamp。新手可以把它理解成一张聊天记录卡片:谁说的、说了什么、什么时候说的,都有固定格子,不再是散乱字符串。

核心源码(逐字来自文末完整源码):

@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

可运行演示(补齐 Mock 数据与 print 反馈):

from dataclasses import dataclass, field
from datetime import datetime


@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

msg = Message(role="user", content="你好,什么是面向对象?")
print("对象原貌:", msg)
print("角色字段:", msg.role)
print("内容字段:", msg.content)

Step 2:用 AgentConfig 集中管理后端和模型参数

痛点与机制:

AgentConfig 把运行参数集中管理。不要让 backend、model、timeout 到处散落,否则后期改模型就像满屋子找遥控器;放进配置类后,上层只需要传一个对象。

核心源码(逐字来自文末完整源码):

@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

可运行演示(补齐 Mock 数据与 print 反馈):

from dataclasses import dataclass

# Step 2:配置对象像遥控器,把 backend/model/temperature 放在一个地方调。
@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

config = AgentConfig(backend="mock", model="mock-v1", temperature=0.3)
print("后端:", config.backend)
print("模型:", config.model)
print("温度:", config.temperature)
print("超时时间:", config.timeout)

Step 3:用 BaseAgent 定义所有智能体都必须遵守的接口合同

痛点与机制:

BaseAgent 不直接干活,它像一份岗位说明书:任何 Agent 子类都必须实现 chat() 和 stream_chat()。这样上层代码不用管底下是 Mock、Ollama 还是未来的 OpenAI,只认同一套接口。

核心源码(逐字来自文末完整源码):

class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"

可运行演示(补齐 Mock 数据与 print 反馈):

import argparse
import time
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Iterator

@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

# Step 3:抽象基类像合同,规定子类必须会 chat 和 stream_chat。
class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"

class EchoAgent(BaseAgent):
    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        reply = f"Echo: {user_input}"
        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        yield self.chat(user_input)

agent = EchoAgent(AgentConfig(backend="echo", model="echo-v1"))
print("回复:", agent.chat("测试抽象类"))
print("调用次数:", agent.call_count)
agent.print_history()

Step 4:用 MockAgent 做零网络依赖的可测试子类

痛点与机制:

MockAgent 的价值不是“假”,而是“稳定可测”。没有 API Key、没有网络、没有本地模型,也能把业务流程跑通。新手先用 MockAgent 学 OOP,比一开始就卡在外部服务上更稳。

核心源码(逐字来自文末完整源码):

class MockAgent(BaseAgent):
    """Mock Agent:用于测试,无需真实 API。"""

    RESPONSES: dict[str, str] = {
        "你好":     "你好!我是 MockAgent,随时为你服务。",
        "python":   "Python 是一门优雅的语言,推荐从类型提示开始学习。",
        "default":  "这是一个模拟回复,用于演示 OOP 多态特性。",
    }

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        time.sleep(0.05)   # 模拟网络延迟

        # 简单关键词匹配
        for keyword, response in self.RESPONSES.items():
            if keyword in user_input.lower():
                reply = response
                break
        else:
            reply = self.RESPONSES["default"]

        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        """流式输出:逐词返回。"""
        full_reply = self.chat(user_input)
        for word in full_reply.split():
            time.sleep(0.05)
            yield word + " "

可运行演示(补齐 Mock 数据与 print 反馈):

import argparse
import time
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Iterator

@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"

# Step 4:MockAgent 像训练用假人,不联网也能练完整流程。
class MockAgent(BaseAgent):
    """Mock Agent:用于测试,无需真实 API。"""

    RESPONSES: dict[str, str] = {
        "你好":     "你好!我是 MockAgent,随时为你服务。",
        "python":   "Python 是一门优雅的语言,推荐从类型提示开始学习。",
        "default":  "这是一个模拟回复,用于演示 OOP 多态特性。",
    }

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        time.sleep(0.05)   # 模拟网络延迟

        # 简单关键词匹配
        for keyword, response in self.RESPONSES.items():
            if keyword in user_input.lower():
                reply = response
                break
        else:
            reply = self.RESPONSES["default"]

        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        """流式输出:逐词返回。"""
        full_reply = self.chat(user_input)
        for word in full_reply.split():
            time.sleep(0.05)
            yield word + " "

agent = MockAgent(AgentConfig(backend="mock", model="mock-v1"))
for question in ["你好", "讲讲 python", "随便问一句"]:
    print("用户:", question)
    print("助手:", agent.chat(question))
print("总调用次数:", agent.call_count)
agent.print_history()

Step 5:用 OllamaAgent 保留真实后端扩展位

痛点与机制:

OllamaAgent 展示的是“系统预留扩展口”。当前示例不真的发网络请求,而是告诉你真实项目会在这里接 requests.post(...)。这样文章既能闭环运行,又能让你看懂以后怎么接本地模型。

核心源码(逐字来自文末完整源码):

class OllamaAgent(BaseAgent):
    """Ollama Agent:调用本地 Ollama 服务(演示版,实际需安装 ollama)。"""

    def __init__(self, config: AgentConfig) -> None:
        super().__init__(config)
        self._base_url = "http://localhost:11434"

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        # 实际调用:import requests; requests.post(f"{self._base_url}/api/chat", ...)
        # 演示版:返回提示信息
        reply = f"[Ollama/{self._config.model}] 需要本地运行 `ollama serve` 才能真实调用。"
        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        yield self.chat(user_input)

可运行演示(补齐 Mock 数据与 print 反馈):

import argparse
import time
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Iterator

@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"

# Step 5:OllamaAgent 演示真实服务接入点,但这里仍保持零依赖闭环。
class OllamaAgent(BaseAgent):
    """Ollama Agent:调用本地 Ollama 服务(演示版,实际需安装 ollama)。"""

    def __init__(self, config: AgentConfig) -> None:
        super().__init__(config)
        self._base_url = "http://localhost:11434"

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        # 实际调用:import requests; requests.post(f"{self._base_url}/api/chat", ...)
        # 演示版:返回提示信息
        reply = f"[Ollama/{self._config.model}] 需要本地运行 `ollama serve` 才能真实调用。"
        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        yield self.chat(user_input)

agent = OllamaAgent(AgentConfig(backend="ollama", model="llama3"))
print("Agent 表示:", agent)
print("助手回复:", agent.chat("你好"))
print("流式输出:", list(agent.stream_chat("你好")))
agent.print_history()

Step 6:用 AgentRouter 按 backend 创建不同子类,实现多态分发

痛点与机制:

AgentRouter 是多态最直观的体现。上层只说“我要 backend=mock”或者“我要 backend=ollama”,路由器负责创建对应对象。后续新增 OpenAIAgent,只要注册进去,上层调用方式不需要大改。

核心源码(逐字来自文末完整源码):

class AgentRouter:
    """
    根据配置选择 Agent 实现。
    上层代码只依赖 BaseAgent 接口,不关心具体实现——这就是多态的价值。
    """

    _registry: dict[str, type[BaseAgent]] = {
        "mock":   MockAgent,
        "ollama": OllamaAgent,
    }

    @classmethod
    def register(cls, name: str, agent_class: type[BaseAgent]) -> None:
        """开放注册:允许外部扩展新的 Agent 类型。"""
        cls._registry[name] = agent_class

    @classmethod
    def create(cls, config: AgentConfig) -> BaseAgent:
        agent_class = cls._registry.get(config.backend)
        if not agent_class:
            available = list(cls._registry.keys())
            raise ValueError(f"未知 backend: {config.backend},可用: {available}")
        return agent_class(config)

可运行演示(补齐 Mock 数据与 print 反馈):

import argparse
import time
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Iterator

@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"

class MockAgent(BaseAgent):
    """Mock Agent:用于测试,无需真实 API。"""

    RESPONSES: dict[str, str] = {
        "你好":     "你好!我是 MockAgent,随时为你服务。",
        "python":   "Python 是一门优雅的语言,推荐从类型提示开始学习。",
        "default":  "这是一个模拟回复,用于演示 OOP 多态特性。",
    }

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        time.sleep(0.05)   # 模拟网络延迟

        # 简单关键词匹配
        for keyword, response in self.RESPONSES.items():
            if keyword in user_input.lower():
                reply = response
                break
        else:
            reply = self.RESPONSES["default"]

        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        """流式输出:逐词返回。"""
        full_reply = self.chat(user_input)
        for word in full_reply.split():
            time.sleep(0.05)
            yield word + " "

class OllamaAgent(BaseAgent):
    """Ollama Agent:调用本地 Ollama 服务(演示版,实际需安装 ollama)。"""

    def __init__(self, config: AgentConfig) -> None:
        super().__init__(config)
        self._base_url = "http://localhost:11434"

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        # 实际调用:import requests; requests.post(f"{self._base_url}/api/chat", ...)
        # 演示版:返回提示信息
        reply = f"[Ollama/{self._config.model}] 需要本地运行 `ollama serve` 才能真实调用。"
        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        yield self.chat(user_input)

# Step 6:路由器像前台分诊,根据 backend 把请求交给不同 Agent。
class AgentRouter:
    """
    根据配置选择 Agent 实现。
    上层代码只依赖 BaseAgent 接口,不关心具体实现——这就是多态的价值。
    """

    _registry: dict[str, type[BaseAgent]] = {
        "mock":   MockAgent,
        "ollama": OllamaAgent,
    }

    @classmethod
    def register(cls, name: str, agent_class: type[BaseAgent]) -> None:
        """开放注册:允许外部扩展新的 Agent 类型。"""
        cls._registry[name] = agent_class

    @classmethod
    def create(cls, config: AgentConfig) -> BaseAgent:
        agent_class = cls._registry.get(config.backend)
        if not agent_class:
            available = list(cls._registry.keys())
            raise ValueError(f"未知 backend: {config.backend},可用: {available}")
        return agent_class(config)

for backend in ["mock", "ollama"]:
    config = AgentConfig(backend=backend, model="demo-model")
    agent = AgentRouter.create(config)
    print(f"{backend} ->", agent.__class__.__name__, "|", agent.chat("你好"))

Step 7:用 ConversationSession 管理多轮对话,并支持 len/bool/iter

痛点与机制:

ConversationSession 把对话历史从 Agent 里单独拎出来,像一个聊天窗口。__len__ 让你能用 len(session),__bool__ 让你能判断有没有内容,__iter__ 让你能直接 for 循环遍历问答。

核心源码(逐字来自文末完整源码):

class ConversationSession:
    """对话会话,演示魔法方法。"""

    def __init__(self, agent: BaseAgent) -> None:
        self._agent = agent
        self._messages: list[tuple[str, str]] = []

    def __len__(self) -> int:
        return len(self._messages)

    def __bool__(self) -> bool:
        return len(self._messages) > 0

    def __iter__(self) -> Iterator[tuple[str, str]]:
        return iter(self._messages)

    def __repr__(self) -> str:
        return f"ConversationSession(agent={self._agent!r}, turns={len(self)})"

    def ask(self, question: str) -> str:
        reply = self._agent.chat(question)
        self._messages.append((question, reply))
        return reply

可运行演示(补齐 Mock 数据与 print 反馈):

import argparse
import time
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Iterator

@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"

class MockAgent(BaseAgent):
    """Mock Agent:用于测试,无需真实 API。"""

    RESPONSES: dict[str, str] = {
        "你好":     "你好!我是 MockAgent,随时为你服务。",
        "python":   "Python 是一门优雅的语言,推荐从类型提示开始学习。",
        "default":  "这是一个模拟回复,用于演示 OOP 多态特性。",
    }

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        time.sleep(0.05)   # 模拟网络延迟

        # 简单关键词匹配
        for keyword, response in self.RESPONSES.items():
            if keyword in user_input.lower():
                reply = response
                break
        else:
            reply = self.RESPONSES["default"]

        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        """流式输出:逐词返回。"""
        full_reply = self.chat(user_input)
        for word in full_reply.split():
            time.sleep(0.05)
            yield word + " "

# Step 7:会话对象像聊天窗口,负责保存一轮一轮的问答。
class ConversationSession:
    """对话会话,演示魔法方法。"""

    def __init__(self, agent: BaseAgent) -> None:
        self._agent = agent
        self._messages: list[tuple[str, str]] = []

    def __len__(self) -> int:
        return len(self._messages)

    def __bool__(self) -> bool:
        return len(self._messages) > 0

    def __iter__(self) -> Iterator[tuple[str, str]]:
        return iter(self._messages)

    def __repr__(self) -> str:
        return f"ConversationSession(agent={self._agent!r}, turns={len(self)})"

    def ask(self, question: str) -> str:
        reply = self._agent.chat(question)
        self._messages.append((question, reply))
        return reply

agent = MockAgent(AgentConfig(backend="mock", model="mock-v1"))
session = ConversationSession(agent)
print("刚创建时是否有内容:", bool(session), "长度:", len(session))
print("回复1:", session.ask("你好"))
print("回复2:", session.ask("介绍一下 Python"))
print("现在是否有内容:", bool(session), "长度:", len(session))
for question, reply in session:
    print(f"问: {question} -> 答: {reply}")
print("会话表示:", session)

Step 8:用 argparse 串起配置、路由、会话和输出

痛点与机制:

main() 是脚本入口,像餐厅经理:先读命令行参数,再创建配置,再用路由器拿到 Agent,最后创建会话并跑演示问题。新手只要学会改 --backend 和 --model,就能控制整套流程。

核心源码(逐字来自文末完整源码):

def main() -> None:
    parser = argparse.ArgumentParser(description="大模型接口封装系统")
    parser.add_argument("--backend", choices=["mock", "ollama"], default="mock")
    parser.add_argument("--model", default="gpt-4o-mini")
    parser.add_argument("--stream", action="store_true", help="流式输出模式")
    args = parser.parse_args()

    config = AgentConfig(backend=args.backend, model=args.model)
    agent = AgentRouter.create(config)

    print(f"\n  🤖 Agent 已就绪: {agent!r}")
    print(f"  配置: backend={config.backend}, model={config.model}")

    session = ConversationSession(agent)

    questions = ["你好", "介绍一下 Python", "什么是面向对象编程"]

    print(f"\n  ── 对话演示 ──────────────────────────────")
    for q in questions:
        print(f"\n  👤 用户: {q}")
        if args.stream:
            print("  🤖 助手: ", end="", flush=True)
            for token in agent.stream_chat(q):
                print(token, end="", flush=True)
            print()
        else:
            reply = session.ask(q)
            print(f"  🤖 助手: {reply}")

    print(f"\n  ── 会话统计 ──────────────────────────────")
    print(f"  {session!r}")
    print(f"  总调用次数: {agent.call_count}")
    print(f"  会话有内容: {bool(session)}")

    agent.print_history()

可运行演示(补齐 Mock 数据与 print 反馈):

import argparse
import time
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Iterator

@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"

@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0

class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"

class MockAgent(BaseAgent):
    """Mock Agent:用于测试,无需真实 API。"""

    RESPONSES: dict[str, str] = {
        "你好":     "你好!我是 MockAgent,随时为你服务。",
        "python":   "Python 是一门优雅的语言,推荐从类型提示开始学习。",
        "default":  "这是一个模拟回复,用于演示 OOP 多态特性。",
    }

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        time.sleep(0.05)   # 模拟网络延迟

        # 简单关键词匹配
        for keyword, response in self.RESPONSES.items():
            if keyword in user_input.lower():
                reply = response
                break
        else:
            reply = self.RESPONSES["default"]

        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        """流式输出:逐词返回。"""
        full_reply = self.chat(user_input)
        for word in full_reply.split():
            time.sleep(0.05)
            yield word + " "

class OllamaAgent(BaseAgent):
    """Ollama Agent:调用本地 Ollama 服务(演示版,实际需安装 ollama)。"""

    def __init__(self, config: AgentConfig) -> None:
        super().__init__(config)
        self._base_url = "http://localhost:11434"

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        # 实际调用:import requests; requests.post(f"{self._base_url}/api/chat", ...)
        # 演示版:返回提示信息
        reply = f"[Ollama/{self._config.model}] 需要本地运行 `ollama serve` 才能真实调用。"
        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        yield self.chat(user_input)

class AgentRouter:
    """
    根据配置选择 Agent 实现。
    上层代码只依赖 BaseAgent 接口,不关心具体实现——这就是多态的价值。
    """

    _registry: dict[str, type[BaseAgent]] = {
        "mock":   MockAgent,
        "ollama": OllamaAgent,
    }

    @classmethod
    def register(cls, name: str, agent_class: type[BaseAgent]) -> None:
        """开放注册:允许外部扩展新的 Agent 类型。"""
        cls._registry[name] = agent_class

    @classmethod
    def create(cls, config: AgentConfig) -> BaseAgent:
        agent_class = cls._registry.get(config.backend)
        if not agent_class:
            available = list(cls._registry.keys())
            raise ValueError(f"未知 backend: {config.backend},可用: {available}")
        return agent_class(config)

class ConversationSession:
    """对话会话,演示魔法方法。"""

    def __init__(self, agent: BaseAgent) -> None:
        self._agent = agent
        self._messages: list[tuple[str, str]] = []

    def __len__(self) -> int:
        return len(self._messages)

    def __bool__(self) -> bool:
        return len(self._messages) > 0

    def __iter__(self) -> Iterator[tuple[str, str]]:
        return iter(self._messages)

    def __repr__(self) -> str:
        return f"ConversationSession(agent={self._agent!r}, turns={len(self)})"

    def ask(self, question: str) -> str:
        reply = self._agent.chat(question)
        self._messages.append((question, reply))
        return reply

# Step 8:main 是总控台,把命令行参数变成完整对话流程。
def main() -> None:
    parser = argparse.ArgumentParser(description="大模型接口封装系统")
    parser.add_argument("--backend", choices=["mock", "ollama"], default="mock")
    parser.add_argument("--model", default="gpt-4o-mini")
    parser.add_argument("--stream", action="store_true", help="流式输出模式")
    args = parser.parse_args()

    config = AgentConfig(backend=args.backend, model=args.model)
    agent = AgentRouter.create(config)

    print(f"\n  🤖 Agent 已就绪: {agent!r}")
    print(f"  配置: backend={config.backend}, model={config.model}")

    session = ConversationSession(agent)

    questions = ["你好", "介绍一下 Python", "什么是面向对象编程"]

    print(f"\n  ── 对话演示 ──────────────────────────────")
    for q in questions:
        print(f"\n  👤 用户: {q}")
        if args.stream:
            print("  🤖 助手: ", end="", flush=True)
            for token in agent.stream_chat(q):
                print(token, end="", flush=True)
            print()
        else:
            reply = session.ask(q)
            print(f"  🤖 助手: {reply}")

    print(f"\n  ── 会话统计 ──────────────────────────────")
    print(f"  {session!r}")
    print(f"  总调用次数: {agent.call_count}")
    print(f"  会话有内容: {bool(session)}")

    agent.print_history()

import sys
sys.argv = ["prog", "--backend", "mock", "--model", "mock-v1"]
main()

极客实战:完整源码与运行

现在,把上面的积木拼起来,将以下完整代码放进你的编辑器,运行它。先看整体闭环,再回头逐段改参数,你会更容易建立工程直觉。

# agent_system.py
"""
大模型接口封装系统 —— 演示 OOP 封装/继承/多态。
用法:
    python3 agent_system.py
    python3 agent_system.py --backend mock
    python3 agent_system.py --backend ollama --model llama3
"""

import argparse
import time
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Iterator


# ── 数据类:消息与响应 ────────────────────────────────────────
@dataclass
class Message:
    role: str       # "user" | "assistant" | "system"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))

    def __str__(self) -> str:
        icon = {"user": "👤", "assistant": "🤖", "system": "⚙️"}.get(self.role, "?")
        return f"[{self.timestamp}] {icon} {self.role}: {self.content}"


@dataclass
class AgentConfig:
    backend: str
    model: str
    temperature: float = 0.7
    max_tokens: int = 512
    timeout: float = 30.0


# ── 抽象基类:定义接口契约 ────────────────────────────────────
class BaseAgent(ABC):
    """所有 Agent 必须实现的接口契约。"""

    def __init__(self, config: AgentConfig) -> None:
        self._config = config
        self._history: list[Message] = []
        self._call_count: int = 0

    @property
    def config(self) -> AgentConfig:
        """只读属性:外部只能读配置,不能直接修改。"""
        return self._config

    @property
    def call_count(self) -> int:
        return self._call_count

    @abstractmethod
    def chat(self, user_input: str) -> str:
        """子类必须实现:发送消息并返回回复。"""
        ...

    @abstractmethod
    def stream_chat(self, user_input: str) -> Iterator[str]:
        """子类必须实现:流式返回回复(逐 token)。"""
        ...

    def add_to_history(self, role: str, content: str) -> None:
        self._history.append(Message(role=role, content=content))

    def clear_history(self) -> None:
        self._history.clear()

    def print_history(self) -> None:
        print(f"\n  对话历史({len(self._history)} 条):")
        for msg in self._history:
            print(f"    {msg}")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(model={self._config.model}, calls={self._call_count})"


# ── 具体子类:Mock Agent(测试用,零网络依赖)────────────────
class MockAgent(BaseAgent):
    """Mock Agent:用于测试,无需真实 API。"""

    RESPONSES: dict[str, str] = {
        "你好":     "你好!我是 MockAgent,随时为你服务。",
        "python":   "Python 是一门优雅的语言,推荐从类型提示开始学习。",
        "default":  "这是一个模拟回复,用于演示 OOP 多态特性。",
    }

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        time.sleep(0.05)   # 模拟网络延迟

        # 简单关键词匹配
        for keyword, response in self.RESPONSES.items():
            if keyword in user_input.lower():
                reply = response
                break
        else:
            reply = self.RESPONSES["default"]

        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        """流式输出:逐词返回。"""
        full_reply = self.chat(user_input)
        for word in full_reply.split():
            time.sleep(0.05)
            yield word + " "


# ── 具体子类:Ollama Agent(本地模型)────────────────────────
class OllamaAgent(BaseAgent):
    """Ollama Agent:调用本地 Ollama 服务(演示版,实际需安装 ollama)。"""

    def __init__(self, config: AgentConfig) -> None:
        super().__init__(config)
        self._base_url = "http://localhost:11434"

    def chat(self, user_input: str) -> str:
        self._call_count += 1
        self.add_to_history("user", user_input)
        # 实际调用:import requests; requests.post(f"{self._base_url}/api/chat", ...)
        # 演示版:返回提示信息
        reply = f"[Ollama/{self._config.model}] 需要本地运行 `ollama serve` 才能真实调用。"
        self.add_to_history("assistant", reply)
        return reply

    def stream_chat(self, user_input: str) -> Iterator[str]:
        yield self.chat(user_input)


# ── 路由器:多态的体现 ────────────────────────────────────────
class AgentRouter:
    """
    根据配置选择 Agent 实现。
    上层代码只依赖 BaseAgent 接口,不关心具体实现——这就是多态的价值。
    """

    _registry: dict[str, type[BaseAgent]] = {
        "mock":   MockAgent,
        "ollama": OllamaAgent,
    }

    @classmethod
    def register(cls, name: str, agent_class: type[BaseAgent]) -> None:
        """开放注册:允许外部扩展新的 Agent 类型。"""
        cls._registry[name] = agent_class

    @classmethod
    def create(cls, config: AgentConfig) -> BaseAgent:
        agent_class = cls._registry.get(config.backend)
        if not agent_class:
            available = list(cls._registry.keys())
            raise ValueError(f"未知 backend: {config.backend},可用: {available}")
        return agent_class(config)


# ── 魔法方法演示 ──────────────────────────────────────────────
class ConversationSession:
    """对话会话,演示魔法方法。"""

    def __init__(self, agent: BaseAgent) -> None:
        self._agent = agent
        self._messages: list[tuple[str, str]] = []

    def __len__(self) -> int:
        return len(self._messages)

    def __bool__(self) -> bool:
        return len(self._messages) > 0

    def __iter__(self) -> Iterator[tuple[str, str]]:
        return iter(self._messages)

    def __repr__(self) -> str:
        return f"ConversationSession(agent={self._agent!r}, turns={len(self)})"

    def ask(self, question: str) -> str:
        reply = self._agent.chat(question)
        self._messages.append((question, reply))
        return reply


# ── 主程序 ────────────────────────────────────────────────────
def main() -> None:
    parser = argparse.ArgumentParser(description="大模型接口封装系统")
    parser.add_argument("--backend", choices=["mock", "ollama"], default="mock")
    parser.add_argument("--model", default="gpt-4o-mini")
    parser.add_argument("--stream", action="store_true", help="流式输出模式")
    args = parser.parse_args()

    config = AgentConfig(backend=args.backend, model=args.model)
    agent = AgentRouter.create(config)

    print(f"\n  🤖 Agent 已就绪: {agent!r}")
    print(f"  配置: backend={config.backend}, model={config.model}")

    session = ConversationSession(agent)

    questions = ["你好", "介绍一下 Python", "什么是面向对象编程"]

    print(f"\n  ── 对话演示 ──────────────────────────────")
    for q in questions:
        print(f"\n  👤 用户: {q}")
        if args.stream:
            print("  🤖 助手: ", end="", flush=True)
            for token in agent.stream_chat(q):
                print(token, end="", flush=True)
            print()
        else:
            reply = session.ask(q)
            print(f"  🤖 助手: {reply}")

    print(f"\n  ── 会话统计 ──────────────────────────────")
    print(f"  {session!r}")
    print(f"  总调用次数: {agent.call_count}")
    print(f"  会话有内容: {bool(session)}")

    agent.print_history()


if __name__ == "__main__":
    main()

终端预期输出:

$ python3 agent_system.py --backend mock

  🤖 Agent 已就绪: MockAgent(model=gpt-4o-mini, calls=0)
  配置: backend=mock, model=gpt-4o-mini

  ── 对话演示 ──────────────────────────────

  👤 用户: 你好
  🤖 助手: 你好!我是 MockAgent,随时为你服务。

  👤 用户: 介绍一下 Python
  🤖 助手: Python 是一门优雅的语言,推荐从类型提示开始学习。

  ── 会话统计 ──────────────────────────────
  ConversationSession(agent=MockAgent(model=gpt-4o-mini, calls=3), turns=3)
  总调用次数: 3
  会话有内容: True

下一篇预告:完成了单个类的拆分与设计,随着代码量的增长,多文件组织势在必行。下一篇《8 · 模块化构建:标准库与自定义包》我们将探讨如何通过模块、包以及最佳目录结构来组织生产级项目。


NexDo Time ⚡

5 分钟极客微操:实现一个 OpenAIAgent(BaseAgent) 子类,用 requests 调用真实的 OpenAI API(https://api.openai.com/v1/chat/completions),API Key 从环境变量 OPENAI_API_KEY 读取,然后用 AgentRouter.register("openai", OpenAIAgent) 注册它。

Don’t wait for next time, do it in the next moment.