简介:本文从工程化视角出发,讲解如何从零搭建一个最小可用的 Python Agent 服务。你将了解配置管理、数据传输对象、模型客户端、服务编排、安全边界与测试反馈的完整设计思路,并掌握在默认不依赖真实模型密钥的前提下进行可重复测试的实践方法,为后续扩展打下坚实基础。
很多开发者在接触 Agent 时,第一反应是写几行代码让模型“说一句话”,跑通便觉得大功告成。然而,这样的脚本距离一个可维护、可测试、可扩展的工程服务还有很长的路。真正有价值的起点,是把模型调用放进规范的工程结构里:配置如何注入、数据如何流转、客户端如何封装、服务如何编排、安全边界如何划定、测试反馈如何闭环。这些要素必须同时出现,服务才算真正立得住。
本文将从零开始,逐步搭出一个最小但完整的 Python Agent 服务骨架,重点不在于模型能力本身,而在于工程结构的完整性。
“最小模型调用”的含义容易被误解。它指的是功能范围最小,而不是工程质量最低。一个最小服务同样应该具备清晰的分层、明确的职责边界和可验证的行为。
如果我们只写一个函数,把密钥、请求、解析、打印全部塞在一起,会立刻遇到几个问题:
因此,“最小”应该理解为:保留一条完整链路,但每层只做一件事。这样做的好处是,后续增加工具调用、对话记忆、多轮编排时,只需在既有骨架上扩展,而不用推倒重来。
一个最小可用的 Agent 服务,可以划分为以下几层:
下面依次展开。
配置的第一原则是:代码不关心值从哪里来,只关心值是否合法。常见做法是使用环境变量配合配置对象。
from dataclasses import dataclassimport os@dataclass(frozen=True)class Settings:model_name: strapi_base: strapi_key: strtimeout_seconds: floatenable_real_model: bool@classmethoddef from_env(cls) -> "Settings":return cls(model_name=os.getenv("AGENT_MODEL", "default-model"),api_base=os.getenv("AGENT_API_BASE", ""),api_key=os.getenv("AGENT_API_KEY", ""),timeout_seconds=float(os.getenv("AGENT_TIMEOUT", "30")),enable_real_model=os.getenv("AGENT_ENABLE_REAL", "false").lower() == "true",)
这里有几个关键点:
enable_real_model 默认关闭,意味着默认测试不会触发真实调用;frozen=True),避免运行期被意外修改;配置层还要避免把密钥写进日志。建议在打印配置时对敏感字段做掩码处理。
DTO(数据传输对象)的职责是定义清晰的输入输出契约。它不包含业务逻辑,只承载数据。
from dataclasses import dataclass, fieldfrom typing import List, Optional@dataclassclass Message:role: strcontent: str@dataclassclass AgentRequest:messages: List[Message]temperature: float = 0.7max_tokens: Optional[int] = None@dataclassclass AgentResponse:content: strmodel: strusage: dict = field(default_factory=dict)raw: Optional[dict] = None
使用 DTO 的好处在于:
在实际项目中,还可以加入 trace id、耗时等字段,为后续可观测性打基础。
客户端层只做一件事:把 DTO 请求发给模型服务,再把返回结果转成 DTO 响应。它不应包含业务判断。
import httpxclass ModelClient:def __init__(self, settings: Settings):self.settings = settingsdef complete(self, request: AgentRequest) -> AgentResponse:if not self.settings.enable_real_model:raise RuntimeError("真实模型调用未开启")payload = {"model": self.settings.model_name,"messages": [m.__dict__ for m in request.messages],"temperature": request.temperature,}headers = {"Authorization": f"Bearer {self.settings.api_key}"}with httpx.Client(timeout=self.settings.timeout_seconds) as client:resp = client.post(self.settings.api_base, json=payload, headers=headers)resp.raise_for_status()data = resp.json()return AgentResponse(content=data["choices"][0]["message"]["content"],model=data.get("model", self.settings.model_name),usage=data.get("usage", {}),raw=data,)
要点说明:
raw 字段,便于排查问题,但不应泄露到外部接口。服务层是 Agent 的核心。它负责组织流程:校验输入、调用客户端、处理异常、整理输出。
class AgentService:def __init__(self, client: ModelClient):self.client = clientdef run(self, request: AgentRequest) -> AgentResponse:if not request.messages:raise ValueError("messages 不能为空")try:response = self.client.complete(request)except Exception as exc:raise RuntimeError(f"模型调用失败: {exc}") from excreturn response
看起来简单,但这一层是后续扩展的关键位置:
服务层应保持无状态(或显式管理状态),以便水平扩展。
安全边界至少覆盖三个方面:
def validate(request: AgentRequest, max_chars: int = 8000) -> None:total = sum(len(m.content) for m in request.messages)if total > max_chars:raise ValueError("输入超过长度限制")for m in request.messages:if m.role not in {"system", "user", "assistant"}:raise ValueError(f"非法角色: {m.role}")
这些检查应在服务层入口处统一执行,而不是散落在各处。
测试策略的核心是:默认路径完全不依赖真实模型。
class FakeClient:def complete(self, request: AgentRequest) -> AgentResponse:return AgentResponse(content="echo: " + request.messages[-1].content,model="fake",)def test_agent_service_echo():service = AgentService(FakeClient())resp = service.run(AgentRequest(messages=[Message("user", "hello")]))assert resp.content == "echo: hello"
对真实模型的测试,则通过环境变量显式开启:
import pytest@pytest.mark.skipif(os.getenv("AGENT_ENABLE_REAL", "false").lower() != "true",reason="未开启真实模型测试",)def test_real_call():settings = Settings.from_env()service = AgentService(ModelClient(settings))resp = service.run(AgentRequest(messages=[Message("user", "ping")]))assert resp.content
这样做的价值在于:
本文从工程结构角度,完整搭建了一个最小 Python Agent 服务:配置集中管理、DTO 定义契约、客户端封装通信、服务层组织逻辑、安全边界守住底线、测试反馈保证可重复验证。默认测试不需要真实密钥,真实调用必须显式开启,这一约定贯穿始终。
这个骨架的价值不在于它现在能做什么,而在于它为后续扩展留好了位置。接下来可以在此基础上逐步加入工具调用、多轮对话、流式输出、可观测性等能力,而每一次扩展都只需在对应层内完成,不会破坏整体结构。工程化的意义,正在于此。