本文目录11 个章节
建立一眼能看懂的仓库
技术简报 Agent 的输入、输出、工具白名单和禁止项已经确定。现在把这张能力边界落到同一个 brief-agent 仓库,先准备模型适配器和领域类型。
brief-agent/
├── src/brief_agent/
│ ├── config.py
│ ├── model.py
│ ├── domain.py
│ ├── runtime.py
│ └── cli.py
├── tests/
├── fixtures/
├── artifacts/
└── pyproject.toml配置、模型适配器和命令行入口先分开。后面加入工具和状态时,入口不会变成所有逻辑的堆放处。
配置只从环境读取一次
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class Settings:
model: str
offline: bool
def load_settings() -> Settings:
return Settings(
model=os.getenv("OPENAI_MODEL", ""),
offline=os.getenv("BRIEF_AGENT_OFFLINE", "0") == "1",
)API Key 由 SDK 的标准环境变量读取,不进入 Settings 的字符串输出,也不写入日志。
定义最小模型协议
from typing import Protocol
class TextModel(Protocol):
def complete(self, prompt: str) -> str: ...
class EchoModel:
def complete(self, prompt: str) -> str:
return f"OFFLINE: {prompt}"EchoModel 没有智能,它保证 CLI、配置和测试在无网络环境仍然可运行。
接通真实 Responses API
from openai import OpenAI
class OpenAITextModel:
def __init__(self, model: str) -> None:
self.client = OpenAI()
self.model = model
def complete(self, prompt: str) -> str:
response = self.client.responses.create(
model=self.model,
input=prompt,
)
return response.output_textCLI ──▶ TextModel.complete ──▶ OpenAI Responses API
│
└────▶ EchoModel(离线)CLI 根据 Settings 选择适配器,输入“解释 Agent Loop”,然后打印结果。此时程序仍是一问一答。
模型适配器的验收
- 离线模式不访问网络。
- 未配置模型时,在线模式在请求前给出清楚错误。
- 日志不包含 API Key 和完整私有 Prompt。
- CLI 只依赖 TextModel 协议。
模型适配器接通后,下面定义 Agent 的主体结构,把目标、状态、行动、观察、工具与停止条件变成 Python 类型。
先画主体结构,再写循环
┌──────── Goal ────────┐
│ 主题、受众、验收规则 │
└──────────┬───────────┘
▼
┌──────── Runtime ───────────────────────────┐
│ State ─▶ Model ─▶ Action ─▶ Tool ─▶ Observation │
│ ▲ │ │
│ └──────────────────────────────────────┘ │
│ Stop Policy / Budget │
└────────────────────────────────────────────┘Goal 描述要完成的任务。State 保存当前进度。Model 根据状态选择 Action。Tool 执行动作并返回 Observation。Runtime 连接所有部件。Stop Policy 决定成功、失败或预算耗尽。
用类型固定 Agent 语言
把下面的领域类型放进 src/brief_agent/domain.py,让模型适配器和运行时共同依赖它们。
from dataclasses import dataclass, field
from typing import Any, Literal
@dataclass(frozen=True)
class Goal:
topic: str
audience: Literal["engineer", "manager"]
@dataclass(frozen=True)
class Action:
kind: Literal["tool", "finish"]
name: str = ""
arguments: dict[str, Any] = field(default_factory=dict)
answer: str = ""
@dataclass(frozen=True)
class Observation:
ok: bool
source: str
data: dict[str, Any]
@dataclass
class AgentState:
goal: Goal
turn: int = 0
observations: list[Observation] = field(default_factory=list)
status: str = "running"这些类型不依赖 OpenAI SDK。模型适配器只需要把当前 State 转成请求,再把模型输出转成 Action。
Runtime 拥有控制权
class DecisionModel:
def decide(self, state: AgentState) -> Action:
raise NotImplementedError
class ToolRunner:
def execute(self, action: Action) -> Observation:
raise NotImplementedError
class StopPolicy:
def check(self, state: AgentState) -> str | None:
if state.turn >= 6:
return "budget_exhausted"
return None模型不能直接改 State,也不能直接调用 Tool。Runtime 接收 Action,检查白名单与预算后才执行,并把 Observation 追加到 State。
一轮运行发生什么
state.turn += 1
│
▼
model.decide(state)
│
├── finish ─▶ validate answer ─▶ succeeded
│
└── tool ───▶ runner.execute ─▶ observation ─▶ stateAction 使用结构化字段,避免解析“请调用某个工具”这类自然语言约定。Observation 同时保存成功与错误,模型下一轮才能看到工具失败。
简单 Agent 暂时不需要哪些部件
第一版没有长期记忆、Planner、多 Agent、MCP 和向量数据库。它只需要一个 Goal、一个 State、一个决策模型、一个工具执行器和一个循环。
省略这些能力不会影响 Agent 的最小结构,反而让每次状态变化都能被打印和测试。
主体结构的验收
- State 可以序列化,不保存客户端连接和函数对象。
- Action 只有
tool与finish两类。 - ToolRunner 拒绝未注册名称。
- StopPolicy 独立于模型输出。
- Runtime 的每次状态变化都生成事件。
第 4 篇沿用这些领域类型,接入第一个 Function Calling 工具,并把模型请求、工具执行和结果回传收进 Agent Loop。
REFERENCES
参考链接
所属系列
AI AGENT 入门教程