本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。
学习目标
学完本节后,你应当能够:
- 实现月份 1 的两个核心接口。
- 理解 FastAPI 路由层如何调用 service 层。
- 确保 CLI 与 API 可以复用相同核心逻辑。
前置知识
- 已完成请求响应模型设计
- 已完成 LLM API 和 Tool Calling 模块
1. 设计原则
月份 1 的路由层只做三件事:
- 接收请求
- 调用 service
- 返回结构化响应
不要在路由层直接写模型请求细节或工具执行细节。
2. 推荐目录
app/
├── api/
│ └── routes.py
├── services/
│ ├── chat_service.py
│ └── tool_service.py
├── clients/
│ └── llm_client.py
└── models.py
3. /chat 接口最小实现思路
service 层
class ChatService:
def __init__(self, llm_client):
self.llm_client = llm_client
async def chat(self, request: ChatRequest) -> ChatResponse:
answer = await self.llm_client.generate(request.messages)
return ChatResponse(answer=answer, model=request.model)
路由层
@router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest) -> ChatResponse:
return await chat_service.chat(request)
4. /tools/run 接口最小实现思路
service 层
class ToolService:
def __init__(self, executor):
self.executor = executor
def run_tool(self, request: ToolRunRequest) -> ToolRunResponse:
result = self.executor.execute(request.tool_name, request.arguments)
return ToolRunResponse(
tool_name=result.tool_name,
success=result.success,
output=result.output,
)
路由层
@router.post("/tools/run", response_model=ToolRunResponse)
async def run_tool(request: ToolRunRequest) -> ToolRunResponse:
return tool_service.run_tool(request)
5. 为什么这层拆分对月份 1 很重要
因为第 4 周的 CLI 入口会直接复用 ChatService 和 ToolService。如果你现在把逻辑写死在 FastAPI 路由里,后面一定要重构。
6. 实操任务
- 写出
ChatService - 写出
ToolService - 写出对应路由
- 用
/docs手动测试
7. 自测题
- 为什么
/chat不应该自己直接拼 HTTP 请求? - 为什么工具执行放到 service 层比放在路由层更合理?
- CLI 想复用
/chat能力时,应该复用哪一层?
8. 作业与验收
验收标准:
/chat能返回结构化响应/tools/run能返回工具执行结果- service 层和路由层职责分明
9. 常见错误
- 路由里直接 new 一堆对象导致耦合过高
- 把 FastAPI 请求对象传到核心业务内部
- API 层和 CLI 层重复实现同一逻辑
10. 本章与前文关系
上一章把 schema 定下来了,这一章开始把真正的功能接进服务:
/chat/tools/run
它也是月份 1 里“分层设计”最应该真正落地的一章。因为这里稍微写乱,后面 CLI、FastAPI 和综合项目会全部受影响。
11. 本章在研发助手项目中的位置
研发助手项目最终会保留两种入口:
- CLI
- FastAPI
这两者如果不能共用同一套 service,后面任何一个功能更新都要改两遍。这是月份 1 明确禁止的情况。
所以本章的真实目标不是“多写两个接口”,而是:
把模型调用和工具调用变成一套可被不同入口复用的业务能力。
12. 路由层、service 层、client 层到底怎么分
路由层
负责:
- 接收 HTTP 请求
- 触发对应 service
- 返回响应模型
service 层
负责:
- 组织业务流程
- 调用 client、tools 等基础组件
- 组装最终业务结果
client 层
负责:
- 与外部模型 API 通信
只要你把这三层分清,后面不管是 FastAPI 还是 CLI,都会非常顺。
13. 错误示例 vs 正确示例
错误示例:路由直接发模型请求
@router.post("/chat")
async def chat(request: ChatRequest):
response = await httpx.AsyncClient().post(...)
...
问题:
- 路由层承担了太多职责
- 无法被 CLI 复用
- 测试边界不清晰
正确示例:路由只调用 service
@router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest) -> ChatResponse:
return await chat_service.chat(request)
这才是月份 1 想让你形成的工程习惯。
14. 完整文件级示例:chat_service.py + tool_service.py + routes.py
app/services/chat_service.py
"""聊天业务逻辑。"""
from __future__ import annotations
from app.models import ChatRequest, ChatResponse
class ChatService:
"""统一承载聊天业务流程。"""
def __init__(self, llm_client) -> None:
self.llm_client = llm_client
async def chat(self, request: ChatRequest) -> ChatResponse:
"""根据请求调用模型并返回结构化响应。"""
raw_messages = [message.model_dump() for message in request.messages]
answer = await self.llm_client.generate(
messages=raw_messages,
temperature=request.temperature,
)
return ChatResponse(
answer=answer,
model=request.model,
)
app/services/tool_service.py
"""工具执行业务逻辑。"""
from __future__ import annotations
from app.models import ToolRunRequest, ToolRunResponse
class ToolService:
"""承载工具执行流程。"""
def __init__(self, executor) -> None:
self.executor = executor
def run_tool(self, request: ToolRunRequest) -> ToolRunResponse:
"""执行工具并返回统一响应。"""
result = self.executor.execute(
tool_name=request.tool_name,
arguments=request.arguments,
)
return ToolRunResponse(
tool_name=result.tool_name,
success=result.success,
output=result.output,
)
app/api/routes.py
"""API 路由定义。"""
from __future__ import annotations
from fastapi import APIRouter
from app.clients.llm_client import LLMClient
from app.models import ChatRequest, ChatResponse, ToolRunRequest, ToolRunResponse
from app.services.chat_service import ChatService
from app.services.tool_service import ToolService
from app.tools.executor import ToolExecutor
from app.tools.registry import ToolRegistry
router = APIRouter()
llm_client = LLMClient()
chat_service = ChatService(llm_client=llm_client)
tool_registry = ToolRegistry()
tool_executor = ToolExecutor(registry=tool_registry)
tool_service = ToolService(executor=tool_executor)
@router.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
@router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest) -> ChatResponse:
return await chat_service.chat(request)
@router.post("/tools/run", response_model=ToolRunResponse)
async def run_tool(request: ToolRunRequest) -> ToolRunResponse:
return tool_service.run_tool(request)
15. 逐段解释这组完整示例
为什么 ChatService 接收的是 ChatRequest
因为它的职责是“处理聊天业务”,而不是理解 HTTP。ChatRequest 已经是业务层可以接受的结构。
为什么 message.model_dump() 出现在 service 层
因为 client 当前只需要普通字典消息体,这是从“业务模型”到“外部 API body”的转换动作,放在 service 层最自然。
为什么 ToolService 不直接操作 FastAPI 对象
因为它未来也要被 CLI 或其他入口复用。
16. 一个更接近月份 1 综合项目的增强方向
后续你可以继续把依赖实例化从 routes.py 中移到单独的装配层,但月份 1 当前阶段,先把边界明确比做复杂依赖注入更重要。
你还可以继续做的增强包括:
- 为
ChatService加日志 - 为
ToolService加错误码映射 - 为
routes.py补统一异常处理
17. 调试与排错:本章最常见问题
问题一:路由能跑,但 CLI 复用困难
通常意味着你把业务逻辑写进路由层了。
问题二:service 和 client 边界不清
典型现象是:service 里直接操作 HTTP 细节,或者 client 里开始夹杂业务语义。
问题三:工具系统在 API 层直接执行细节过多
这会让路由层变得脆弱。
18. 本章完成后你应该具备的能力
完成本章后,你应当做到:
- 能解释三层分工。
- 能实现
/chat和/tools/run。 - 能保证未来 CLI 与 API 共用业务层。
- 能识别“接口能跑但结构已经坏了”的早期信号。
19. 如果你卡在这里,先回看哪几章
- FastAPI 最小服务不稳:回看 01-FastAPI最小服务.md
- Tool system 不稳:回看 02-工具循环实现.md
- LLM client 不稳:回看 01-LLM基本概念与DeepSeek接入.md
20. 从本章过渡到下一章的桥接说明
接下来进入 04-流式响应与接口测试.md。
因为你现在已经完成最小接口主链路,下一步要补的是:
- 更接近真实用户体验的流式响应
- 更接近真实工程的接口测试
这会让你的服务从“能调用”走向“更可靠、更能展示”。