← Back to 文字

`/chat` 与 `/tools/run` 接口实现

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 实现月份 1 的两个核心接口。
  • 理解 FastAPI 路由层如何调用 service 层。
  • 确保 CLI 与 API 可以复用相同核心逻辑。

前置知识

  • 已完成请求响应模型设计
  • 已完成 LLM API 和 Tool Calling 模块

1. 设计原则

月份 1 的路由层只做三件事:

  1. 接收请求
  2. 调用 service
  3. 返回结构化响应

不要在路由层直接写模型请求细节或工具执行细节。

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 入口会直接复用 ChatServiceToolService。如果你现在把逻辑写死在 FastAPI 路由里,后面一定要重构。

6. 实操任务

  1. 写出 ChatService
  2. 写出 ToolService
  3. 写出对应路由
  4. /docs 手动测试

7. 自测题

  1. 为什么 /chat 不应该自己直接拼 HTTP 请求?
  2. 为什么工具执行放到 service 层比放在路由层更合理?
  3. 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. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能解释三层分工。
  2. 能实现 /chat/tools/run
  3. 能保证未来 CLI 与 API 共用业务层。
  4. 能识别“接口能跑但结构已经坏了”的早期信号。

19. 如果你卡在这里,先回看哪几章

20. 从本章过渡到下一章的桥接说明

接下来进入 04-流式响应与接口测试.md

因为你现在已经完成最小接口主链路,下一步要补的是:

  • 更接近真实用户体验的流式响应
  • 更接近真实工程的接口测试

这会让你的服务从“能调用”走向“更可靠、更能展示”。

Fin