简介:本文将带你从零构建一个可独立运行的 Python Agent 服务,而非一次性模型调用脚本。你将获得一个具备健康检查、输入护栏、RAG 链路、工具编排与测试开关的工程化服务骨架,可直接被后端系统通过 HTTP 调用。文章重点不在于“让模型说一句话”,而在于把模型调用放进配置、DTO、客户端、服务编排、安全边界与测试反馈共存的工程结构中,为后续接入真实模型与业务链路打下可靠基础。
很多开发者在第一次接触大模型应用开发时,习惯性地写一段脚本,把 Prompt 丢给模型,再把返回结果打印出来。这种方式用来验证模型可用性没有问题,但它距离一个真正可以被业务系统集成的 Agent 服务还有明显距离。
本章要完成的事情,不是再写一段“调用—打印—结束”的脚本,而是搭建一个可以独立运行的 Python Agent Service。它具备以下能力:
/health 接口可以返回服务运行状态和当前模型配置;/agent/chat 接口可以接收用户消息,先执行输入护栏,再进入 RAG、工具调用和主模型链路;/agent/today 接口可以基于用户档案与打卡记录生成当日计划;/rag/ingest 接口可以将文本切块并写入最小 RAG 存储;这个服务后续可以被 Spring Boot 等后端系统通过 HTTP 协议直接调用。换句话说,它不是一个“模型 Demo”,而是一个具备基本工程形态的 Agent 服务骨架。
在真实项目中,模型调用只占整个系统的一小部分。更多的工作量分布在配置管理、数据传输对象设计、客户端封装、服务编排、异常处理、安全边界和测试验证等环节。如果这些环节缺失,模型能力再强,也很难稳定地融入业务系统。
本章的核心关注点可以概括为六个关键词:
只有把这些模块同时放进项目里,才算真正把模型调用“工程化”了。
为了让整个搭建过程清晰可控,本章会涉及三类代码块,必须严格区分:
这类代码块用于在终端中运行,例如安装依赖、启动服务、执行测试等。请直接复制到终端执行。
# 示例:安装项目依赖pip install -r requirements.txt
这类代码块需要把内容完整复制到指定文件中。文件名会在代码块上方或内部明确标注。
# 示例:app/config.pyclass ModelConfig:provider: str = "mock"api_key: str = ""base_url: str = ""temperature: float = 0.7max_tokens: int = 2048
这类代码块仅用于解释某个设计决策或执行流程,不需要写入项目。它们通常是对应模块的简化示意,帮助理解代码为什么“长成这样”。
# 理解片段:请求处理流程示意用户消息 -> 输入护栏 -> RAG 检索 -> 工具调用 -> 主模型生成 -> 响应 DTO
每完成 2 到 4 个文件,就应该运行一次轻量验证。不要等整章结束才测试,否则错误会堆积在一起,定位成本会显著上升。
一个清晰的项目结构是后续所有工作的基础。推荐采用以下目录组织方式:
agent-service/├── app/│ ├── __init__.py│ ├── main.py # FastAPI 应用入口│ ├── config.py # 配置管理│ ├── dto/│ │ ├── __init__.py│ │ ├── chat.py # 聊天请求/响应 DTO│ │ ├── today.py # 今日计划 DTO│ │ └── ingest.py # RAG 写入 DTO│ ├── client/│ │ ├── __init__.py│ │ ├── base.py # 模型客户端抽象│ │ └── mock_client.py # 模拟模型客户端│ ├── guard/│ │ ├── __init__.py│ │ └── input_guard.py # 输入护栏│ ├── rag/│ │ ├── __init__.py│ │ ├── store.py # 最小 RAG 存储│ │ └── ingest.py # 文本切块与写入│ ├── tool/│ │ ├── __init__.py│ │ └── registry.py # 工具注册表│ └── service/│ ├── __init__.py│ ├── chat_service.py # 聊天服务编排│ └── today_service.py # 今日计划服务编排├── tests/│ ├── __init__.py│ ├── test_health.py│ ├── test_chat.py│ └── test_rag.py├── requirements.txt└── README.md
这个结构遵循“关注点分离”原则:配置、传输对象、模型客户端、服务编排、安全护栏和 RAG 存储各自独立,后续扩展时不会互相干扰。
配置模块负责加载和管理所有与模型相关的参数。它需要区分“模拟模式”和“真实模式”,默认使用模拟模式,避免在开发和测试阶段误调用真实模型接口。
# app/config.pyfrom dataclasses import dataclass, fieldfrom enum import Enumimport osclass ModelMode(str, Enum):MOCK = "mock"REAL = "real"@dataclassclass ModelConfig:mode: ModelMode = ModelMode.MOCKapi_key: str = ""base_url: str = ""model_name: str = "default-model"temperature: float = 0.7max_tokens: int = 2048@dataclassclass AppConfig:model: ModelConfig = field(default_factory=ModelConfig)rag_store_path: str = "./data/rag_store.json"def load_config() -> AppConfig:config = AppConfig()# 真实模型测试必须显式开启mode = os.getenv("AGENT_MODEL_MODE", ModelMode.MOCK.value)config.model.mode = ModelMode(mode)config.model.api_key = os.getenv("AGENT_MODEL_API_KEY", "")config.model.base_url = os.getenv("AGENT_MODEL_BASE_URL", "")config.model.model_name = os.getenv("AGENT_MODEL_NAME", "default-model")return config
这里有几个设计要点值得注意:
在服务化场景中,HTTP 接口的请求体和响应体应当有明确的类型定义。这不仅能提升可读性,也能帮助接口文档自动生成。
# app/dto/chat.pyfrom pydantic import BaseModel, Fieldfrom typing import List, Optionalclass ChatRequest(BaseModel):message: str = Field(..., min_length=1, max_length=2000, description="用户输入消息")session_id: Optional[str] = Field(None, description="会话标识,可选")user_profile: Optional[dict] = Field(None, description="用户档案,可选")class ChatResponse(BaseModel):code: int = 0message: str = "success"data: Optional[dict] = Noneguard_triggered: bool = Falseguard_reason: Optional[str] = None
DTO 层使用 Pydantic 进行校验,可以在进入业务逻辑之前拦截非法输入。例如,message 字段限定了最小长度和最大长度,超过范围会直接返回 422 错误。这属于第一层“被动防护”。
模型客户端负责与底层模型进行交互。在工程实践中,建议先定义抽象基类,再提供模拟实现和真实实现。这样可以在没有真实密钥的情况下完成全部链路调试。
# app/client/base.pyfrom abc import ABC, abstractmethodfrom typing import Anyclass BaseModelClient(ABC):@abstractmethoddef chat(self, messages: list[dict[str, str]], **kwargs) -> str:"""发送对话消息并返回模型回复"""passclass MockModelClient(BaseModelClient):"""模拟模型客户端,用于本地开发与测试"""def chat(self, messages: list[dict[str, str]], **kwargs) -> str:# 简单回显最后一条用户消息,便于链路调试last_user_msg = ""for msg in reversed(messages):if msg.get("role") == "user":last_user_msg = msg.get("content", "")breakreturn f"[mock-reply] {last_user_msg}"
模拟客户端的行为保持极简,目的是让链路跑通,而不是模拟真实模型的所有特性。后续接入真实模型时,只需新增一个继承 BaseModelClient 的客户端类,并在配置中切换即可。
输入护栏(Input Guard)负责在用户消息进入主链路之前进行安全检查。这里的“安全检查”包括但不限于:长度校验、敏感词过滤、Prompt 注入特征识别、异常字符检测等。
# app/guard/input_guard.pyfrom dataclasses import dataclass@dataclassclass GuardResult:passed: boolreason: str = ""class InputGuard:BLOCKED_PATTERNS = ["忽略以上指令","ignore previous instructions","system prompt","系统提示词",]def check(self, message: str) -> GuardResult:if not message or not message.strip():return GuardResult(passed=False, reason="empty message")if len(message) > 2000:return GuardResult(passed=False, reason="message too long")lowered = message.lower()for pattern in self.BLOCKED_PATTERNS:if pattern.lower() in lowered:return GuardResult(passed=False, reason=f"blocked pattern: {pattern}")return GuardResult(passed=True)
输入护栏的逻辑应当保持独立,便于后续扩展。例如可以增加正则规则、调用外部审核接口、或者接入向量检索识别恶意输入。关键原则是:所有用户输入在进入模型之前,都必须经过这道闸门。
RAG(Retrieval-Augmented Generation,检索增强生成)的核心思路是:先从外部知识库中检索相关文本片段,再把检索结果作为上下文交给模型生成回复。本章实现一个最小化的 RAG 存储和写入接口。
# app/rag/store.pyimport jsonimport osfrom typing import List, Dict, Anyclass SimpleRagStore:def __init__(self, store_path: str):self.store_path = store_pathself.chunks: List[Dict[str, Any]] = []self._load()def _load(self):if os.path.exists(self.store_path):with open(self.store_path, "r", encoding="utf-8") as f:self.chunks = json.load(f)def _save(self):os.makedirs(os.path.dirname(self.store_path), exist_ok=True)with open(self.store_path, "w", encoding="utf-8") as f:json.dump(self.chunks, f, ensure_ascii=False, indent=2)def add(self, chunk: Dict[str, Any]):self.chunks.append(chunk)self._save()def search(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]:# 简化实现:按文本重叠度进行关键词匹配query_terms = set(query.lower().split())scored = []for chunk in self.chunks:text = chunk.get("text", "").lower()overlap = len(query_terms & set(text.split()))if overlap > 0:scored.append((overlap, chunk))scored.sort(key=lambda x: x[0], reverse=True)return [item[1] for item in scored[:top_k]]def list_chunks(self) -> List[Dict[str, Any]]:return self.chunks
文本切块逻辑放在 rag/ingest.py 中:
# app/rag/ingest.pyfrom typing import Listdef split_text(text: str, chunk_size: int = 500, overlap: int = 50) -> List[str]:"""简单固定长度切块,带重叠"""if not text:return []chunks = []start = 0text_len = len(text)while start < text_len:end = min(start + chunk_size, text_len)chunks.append(text[start:end])if end == text_len:breakstart = end - overlapreturn chunks
这里的实现刻意保持简单,目的是让 RAG 链路在本地跑通。生产环境中可以替换为向量检索方案,但这不影响整体服务架构的设计。
服务编排层是整个 Agent 的核心。它负责把输入护栏、RAG 检索、工具调用和模型生成按正确顺序组装起来。
# app/service/chat_service.pyfrom app.guard.input_guard import InputGuard, GuardResultfrom app.client.base import BaseModelClientfrom app.rag.store import SimpleRagStorefrom app.dto.chat import ChatRequest, ChatResponseclass ChatService:def __init__(self, model_client: BaseModelClient, rag_store: SimpleRagStore):self.model_client = model_clientself.rag_store = rag_storeself.input_guard = InputGuard()def process(self, request: ChatRequest) -> ChatResponse:# 第一步:输入护栏guard_result: GuardResult = self.input_guard.check(request.message)if not guard_result.passed:return ChatResponse(code=400,message="guard rejected",guard_triggered=True,guard_reason=guard_result.reason,)# 第二步:RAG 检索rag_chunks = self.rag_store.search(request.message, top_k=3)context_text = "\n".join([c.get("text", "") for c in rag_chunks])# 第三步:组装消息messages = []if context_text:messages.append({"role": "system","content": f"以下是从知识库检索到的相关信息,请在回答时参考:\n{context_text}"})if request.user_profile:messages.append({"role": "system","content": f"当前用户档案:{request.user_profile}"})messages.append({"role": "user", "content": request.message})# 第四步:主模型生成reply = self.model_client.chat(messages)return ChatResponse(code=0,message="success",data={"reply": reply, "rag_chunks_used": len(rag_chunks)},)
可以看到,服务编排层只负责“编排”,不直接实现具体业务逻辑。输入校验在 Guard 层,检索逻辑在 RAG 层,模型交互在 Client 层。这种分层让代码更易测试、更容易替换组件。
接口层负责接收 HTTP 请求,把它们转换为 DTO,调用服务层,再返回 JSON 响应。
# app/main.pyfrom fastapi import FastAPI, Dependsfrom app.config import load_config, AppConfigfrom app.client.base import MockModelClientfrom app.rag.store import SimpleRagStorefrom app.service.chat_service import ChatServicefrom app.dto.chat import ChatRequest, ChatResponseapp = FastAPI(title="Agent Service", version="0.1.0")_config: AppConfig = load_config()_model_client = MockModelClient()_rag_store = SimpleRagStore(_config.rag_store_path)_chat_service = ChatService(_model_client, _rag_store)@app.get("/health")def health():return {"status": "ok","model_mode": _config.model.mode.value,"model_name": _config.model.model_name,}@app.post("/agent/chat", response_model=ChatResponse)def agent_chat(request: ChatRequest):return _chat_service.process(request)@app.post("/rag/ingest")def rag_ingest(text: str, chunk_size: int = 500):from app.rag.ingest import split_textchunks = split_text(text, chunk_size)for i, chunk in enumerate(chunks):_rag_store.add({"id": f"chunk_{i}", "text": chunk})return {"code": 0, "message": "success", "data": {"chunk_count": len(chunks)}}
/health 接口返回服务状态和当前模型配置,方便运维侧快速确认服务是否正常。/agent/chat 接口承担主要对话功能。/rag/ingest 接口负责将外部文本写入 RAG 存储。
每完成 2 到 4 个文件,就运行一次轻量验证,这一点怎么强调都不过分。推荐的验证节奏如下:
在完成 config.py 后,在终端中运行:
python -c "from app.config import load_config; print(load_config())"
预期输出应显示配置对象,模型模式为 mock。
完成 DTO 后,可以快速验证非法输入是否被拦截:
python -c "from app.dto.chat import ChatRequest; ChatRequest(message='')"
应当抛出校验错误。
完成 RAG 模块后,测试文本切块和存储:
python -c "from app.rag.ingest import split_textfrom app.rag.store import SimpleRagStorestore = SimpleRagStore('./data/test_rag.json')for i, c in enumerate(split_text('这是一段用于测试的文本内容。' * 100)):store.add({'id': str(i), 'text': c})print(len(store.list_chunks()))"
完成服务和接口层后,启动服务并测试:
uvicorn app.main:app --host 0.0.0.0 --port 8000
然后在另一个终端中测试:
curl -X POST http://localhost:8000/agent/chat \-H "Content-Type: application/json" \-d '{"message": "你好"}'
预期返回模拟模型的回复。
/agent/today 接口展示了 Agent 服务在“非纯对话”场景下的应用。它需要结合用户档案和打卡记录生成当日计划。核心流程如下:
读取用户档案 -> 读取当日打卡记录 -> 组装 Prompt -> 调用模型生成计划 -> 返回结构化结果
# app/service/today_service.pyfrom app.client.base import BaseModelClientfrom app.dto.today import TodayPlanRequest, TodayPlanResponseclass TodayService:def __init__(self, model_client: BaseModelClient):self.model_client = model_clientdef generate(self, request: TodayPlanRequest) -> TodayPlanResponse:# 组装上下文messages = [{"role": "system","content": "你是一个个人日程规划助手。根据用户档案和打卡记录,生成今日计划。"},{"role": "user","content": (f"用户档案:{request.user_profile}\n"f"今日打卡记录:{request.check_in_records}\n""请生成今日计划。")}]plan_text = self.model_client.chat(messages)return TodayPlanResponse(code=0,message="success",data={"plan": plan_text})
这个接口的价值在于展示 Agent 服务如何编排多个输入源。后续可以将打卡记录替换为来自数据库或第三方系统 API 的数据,模型调用部分无需改动。
本章搭建的是一个最小可运行的 Agent 服务骨架。它成功把模型调用放进了工程结构中,但要走向生产环境,还需要在以下方向持续演进:
这些演进方向并不是一次性完成,而是随着业务需求逐步推进。本章的意义在于提供一个正确的起点:模型调用从一开始就处于工程化框架之中,而不是游离在业务系统之外。
通过本章的实践,你获得的不是一个“会说话的脚本”,而是一个可以独立运行的 Python Agent Service。它具备清晰的分层结构、明确的接口边界、基础的安全防护和可扩展的 RAG 链路。更重要的是,它遵循了渐进式开发原则——每完成几个文件就验证一次,让错误在第一时间被发现和修复。
这个最小骨架为后续接入真实模型、扩展复杂工具链、对接企业后端系统提供了可靠的地基。无论你最终选择哪家模型服务商,这套工程结构都能让你在“调通模型”之后,真正迈出“服务化”的关键一步。