前端转 AI 第 24 天:API 测试 - 用 httpx + pytest 测 FastAPI
昨日回顾
Day 23 把数据库层升级到生产级:请求级 Session、Alembic 迁移、连接池。
但有个问题 ------API 层一行测试都没有。
Day 18-19 测的是业务层(storage_orm.py / memory.py),API 层是"接口契约",最容易出问题:
- 改了参数名,前端不知道
- 错误格式变了,前端处理崩了
- 加了认证,忘了某个接口也要加
今天给 API 写一套完整测试。
Day 24 目标:掌握 FastAPI 测试的三件套(TestClient / 依赖覆盖 / 临时数据库),给 API 全链路测试。
一、为什么 API 测试和单元测试不一样
单元测试 :测函数逻辑,隔离一切。
API 测试 :测"请求进来,响应出去",中间尽量真实。
| 关注点 | 单元测试 | API 测试 |
|---|---|---|
| 测试对象 | 函数 / 类 | HTTP 端点 |
| 输入 | 参数 | HTTP 请求(URL / Header / Body) |
| 输出 | 返回值 | HTTP 响应(状态码 / Body / Header) |
| 关注点 | 逻辑正确 | 契约正确 + 状态码正确 |
| Mock 程度 | 全 mock | 只 mock 外部(LLM / 第三方) |
| 数据库 | mock | 用真数据库(临时) |
API 测试是"集成测试"------从请求到响应,中间所有层都跑一遍。
为什么必须做?
- 契约是第一位的 :前端不看你源码,只看
/docs。改了字段名,前端不知道 - 状态码是契约的一部分:404 / 422 / 500 前端要分别处理
- 认证 / 中间件是横切逻辑:容易漏掉某个接口
- 比手动测试靠谱:Postman 会忘记、会漏,代码不会
二、FastAPI 测试三件套
FastAPI 官方推荐用 TestClient,我们再加两个关键工具。
1. TestClient:同步客户端
安装:
bash
pip install httpx
FastAPI 的
TestClient基于httpx,所以要装它。
写法:
python
from fastapi.testclient import TestClient
from myapp.main import app
client = TestClient(app)
def test_health():
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
看起来就像发真实请求,但不会启动服务器------FastAPI 拦截请求,直接调内部逻辑。
2. AsyncClient:异步客户端
要测异步接口时用:
python
import pytest
from httpx import AsyncClient, ASGITransport
from myapp.main import app
@pytest.mark.asyncio
async def test_health_async():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/health")
assert response.status_code == 200
区别:
| 类型 | 用法 | 适合 |
|---|---|---|
TestClient |
同步,简单 | 90% 场景 |
AsyncClient |
异步,await |
需要测并发 / 原生异步 |
建议:先用 TestClient ,够用。有明确异步需求再换 AsyncClient。
3. 依赖覆盖:dependency_overrides
最关键的技能。 测试时把"真实依赖"换成"测试依赖"。
python
def get_db():
return RealDatabase() # 生产用的
def get_fake_db():
return FakeDatabase() # 测试用的
# 测试时替换
app.dependency_overrides[get_db] = get_fake_db
这让你能:
- 不污染生产数据库:用临时数据库
- 不真调 LLM:用 Mock 客户端
- 不需要环境变量:认证直接通过
用法:
python
@pytest.fixture
def client():
app.dependency_overrides[get_db] = get_test_db
with TestClient(app) as c:
yield c
app.dependency_overrides.clear() # 清理,避免污染其他测试
这个 fixture 是 API 测试的基础。
三、测试项目结构
bash
cp -r day23/ai-assistant-api-v3 day24/ai-assistant-api-v4
cd day24/ai-assistant-api-v4
新增测试文件:
bash
tests/
├── conftest.py # 已有(加 API 相关 fixture)
├── test_storage.py # 已有
├── test_memory.py # 已有
├── api/ # ← 新增
│ ├── __init__.py
│ ├── conftest.py # API 专用 fixture
│ ├── test_health.py # 健康检查
│ ├── test_chat.py # 对话接口
│ ├── test_sessions.py # 会话管理
│ └── test_auth.py # 认证
为什么单独建 api/ 目录?
- 隔离:API 测试的 fixture 和业务测试不同
- 组织:测试文件多了要分类
- 独立运行 :
pytest tests/api/只跑 API 测试
四、API 测试 fixture
核心:conftest.py 里的"测试客户端"fixture。
1. tests/api/conftest.py
python
"""API 测试专用 fixture"""
import pytest
from pathlib import Path
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from ai_assistant.api.app import app
from ai_assistant.api.deps import (
get_database, get_db_session, get_llm, get_memory,
)
from ai_assistant.api.auth import verify_api_key
from ai_assistant.db_orm import DatabaseORM
from ai_assistant.orm_models import Base
from ai_assistant.llm import MockLLMClient
from ai_assistant.memory import MemoryManager
@pytest.fixture
def test_db(tmp_path):
"""测试用的数据库(独立文件)"""
db_path = tmp_path / "test.db"
db = DatabaseORM.__new__(DatabaseORM) # 不走 __init__ 的默认路径
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
db.engine = create_engine(
f"sqlite:///{db_path}",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
db.SessionLocal = sessionmaker(bind=db.engine, expire_on_commit=False)
Base.metadata.create_all(db.engine)
yield db
db.close()
@pytest.fixture
def test_session(test_db):
"""请求级 Session(模拟 get_db_session)"""
session = test_db.SessionLocal()
try:
yield session
finally:
session.close()
@pytest.fixture
def fake_llm():
"""假的 LLM 客户端"""
llm = MockLLMClient(delay=0) # 无延迟,测试快
return llm
@pytest.fixture
def client(test_db, fake_llm):
"""测试客户端(覆盖所有依赖)"""
from sqlalchemy.orm import sessionmaker
# 1. 覆盖数据库
def override_get_database():
return test_db
def override_get_db_session():
session = test_db.SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def override_get_llm():
return fake_llm
def override_get_memory():
return MemoryManager(
max_context_tokens=8000,
recent_window=10,
summary_threshold=20,
)
def override_verify_api_key():
"""跳过认证"""
return "test-key"
app.dependency_overrides[get_database] = override_get_database
app.dependency_overrides[get_db_session] = override_get_db_session
app.dependency_overrides[get_llm] = override_get_llm
app.dependency_overrides[get_memory] = override_get_memory
app.dependency_overrides[verify_api_key] = override_verify_api_key
with TestClient(app) as c:
yield c
# 清理
app.dependency_overrides.clear()
逐块拆解:
1. test_db fixture
每次测试生成独立的数据库文件 (tmp_path 下),测完自动删除。
关键技巧:DatabaseORM.__new__(DatabaseORM)
不走 __init__ (否则会连到 data/assistant.db),直接构造一个"空壳",然后手动设置 engine 和 SessionLocal。
2. test_session fixture
模拟请求级 Session------测试里需要直接操作数据库时用。
3. fake_llm fixture
用 MockLLMClient(delay=0)------Day 14 写的假客户端,测试里正好用上。
延迟设为 0 让测试跑得飞快。
4. client fixture
核心------覆盖所有依赖:
get_database→ 测试数据库get_db_session→ 测试 Sessionget_llm→ 假 LLMget_memory→ 测试用 MemoryManagerverify_api_key→ 跳过认证
with TestClient(app) as c:确保 lifespan 正确执行。
app.dependency_overrides.clear():每个测试后清理,避免污染。
2. 为什么用 dependency_overrides 而不是 monkeypatch
monkeypatch 也行,但 dependency_overrides 更"官方":
| 方式 | 优点 | 缺点 |
|---|---|---|
monkeypatch |
通用 | 侵入性强,容易漏 |
dependency_overrides |
FastAPI 官方 | 只对 Depends 生效 |
FastAPI 官方推荐用 dependency_overrides------它精确、可控、清理干净。
五、写第一个 API 测试
1. tests/api/test_health.py
python
"""健康检查测试"""
class TestHealth:
def test_health_returns_ok(self, client):
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_health_has_request_id(self, client):
response = client.get("/health")
# 中间件加的
assert "x-request-id" in response.headers
def test_health_has_process_time(self, client):
response = client.get("/health")
assert "x-process-time" in response.headers
assert float(response.headers["x-process-time"]) >= 0
这几个测试验证了 Day 22 的中间件生效了。 别小看它------中间件最容易"改坏而不自知"。
2. tests/api/test_chat.py
python
"""对话接口测试"""
import pytest
class TestChat:
def test_chat_creates_new_session(self, client):
"""不传 session_id,自动创建新会话"""
response = client.post("/api/chat", json={"message": "你好"})
assert response.status_code == 200
data = response.json()
assert "session_id" in data
assert len(data["session_id"]) == 8
assert data["message_count"] == 2 # 用户 + 助手
assert len(data["reply"]) > 0
def test_chat_continues_session(self, client):
"""传 session_id,继续会话"""
# 第一次
r1 = client.post("/api/chat", json={"message": "你好"})
session_id = r1.json()["session_id"]
# 第二次
r2 = client.post(
"/api/chat",
json={"message": "继续", "session_id": session_id},
)
assert r2.status_code == 200
data = r2.json()
assert data["session_id"] == session_id
assert data["message_count"] == 4 # 累积
def test_chat_nonexistent_session(self, client):
"""session_id 不存在"""
response = client.post(
"/api/chat",
json={"message": "你好", "session_id": "nonexistent"},
)
assert response.status_code == 404
data = response.json()
assert "不存在" in data["message"]
@pytest.mark.parametrize("bad_payload", [
{}, # 缺 message
{"message": ""}, # 空字符串
{"message": "x" * 20000}, # 超长
{"message": 123}, # 类型错
{"message": "hi", "session_id": 123}, # session_id 类型错
])
def test_chat_validation_errors(self, client, bad_payload):
"""校验错误都返回 422"""
response = client.post("/api/chat", json=bad_payload)
assert response.status_code == 422
data = response.json()
assert data["code"] == 422
assert data["message"] == "请求参数校验失败"
assert isinstance(data["details"], list)
这几个测试覆盖了核心场景:
- 新建会话
- 续会话
- 会话不存在
- 参数校验(5 种错误场景)
参数化特别适合"边界测试"------一条参数一行,清晰。
3. tests/api/test_sessions.py
python
"""会话管理测试"""
@pytest.fixture
def sample_session_data(client):
"""造一个带内容的会话,返回 session_id"""
r = client.post("/api/chat", json={"message": "测试会话"})
return r.json()["session_id"]
class TestListSessions:
def test_empty_list(self, client):
response = client.get("/api/sessions")
assert response.status_code == 200
data = response.json()
assert data["sessions"] == []
assert data["total"] == 0
def test_list_with_sessions(self, client, sample_session_data):
response = client.get("/api/sessions")
data = response.json()
assert data["total"] == 1
assert data["sessions"][0]["id"] == sample_session_data
class TestGetSession:
def test_get_existing(self, client, sample_session_data):
response = client.get(f"/api/sessions/{sample_session_data}")
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_session_data
assert len(data["messages"]) == 2
assert data["messages"][0]["role"] == "user"
assert data["messages"][1]["role"] == "assistant"
def test_get_nonexistent(self, client):
response = client.get("/api/sessions/nonexistent")
assert response.status_code == 404
class TestDeleteSession:
def test_delete_existing(self, client, sample_session_data):
response = client.delete(f"/api/sessions/{sample_session_data}")
assert response.status_code == 204
# 再查,应该 404
response = client.get(f"/api/sessions/{sample_session_data}")
assert response.status_code == 404
def test_delete_nonexistent(self, client):
response = client.delete("/api/sessions/nonexistent")
assert response.status_code == 404
class TestSearch:
def test_search_finds(self, client, sample_session_data):
response = client.get("/api/search", params={"q": "测试"})
assert response.status_code == 200
data = response.json()
assert data["count"] >= 1
assert any(r["id"] == sample_session_data for r in data["results"])
def test_search_no_result(self, client):
response = client.get("/api/search", params={"q": "不存在的词汇xyz"})
assert response.status_code == 200
assert response.json()["count"] == 0
def test_search_missing_query(self, client):
"""缺 q 参数"""
response = client.get("/api/search")
assert response.status_code == 422
这些测试覆盖了完整的 CRUD + 搜索。
4. tests/api/test_auth.py
认证测试需要"不覆盖"认证依赖------单独写 client fixture:
python
"""认证测试"""
import os
import pytest
from fastapi.testclient import TestClient
from ai_assistant.api.app import app
from ai_assistant.api.deps import get_database, get_db_session, get_llm, get_memory
@pytest.fixture
def auth_client(test_db, fake_llm, monkeypatch):
"""需要真实认证的客户端"""
# 设置 API_KEY
monkeypatch.setenv("API_KEY", "test-secret-key")
def override_get_database():
return test_db
def override_get_db_session():
session = test_db.SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def override_get_llm():
return fake_llm
app.dependency_overrides[get_database] = override_get_database
app.dependency_overrides[get_db_session] = override_get_db_session
app.dependency_overrides[get_llm] = override_get_llm
# 注意:不覆盖 verify_api_key!
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
class TestApiKey:
def test_missing_key_returns_401(self, auth_client):
response = auth_client.get("/api/sessions")
assert response.status_code == 401
data = response.json()
assert "API Key" in data["message"]
def test_wrong_key_returns_401(self, auth_client):
response = auth_client.get(
"/api/sessions",
headers={"X-API-Key": "wrong-key"},
)
assert response.status_code == 401
def test_correct_key_passes(self, auth_client):
response = auth_client.get(
"/api/sessions",
headers={"X-API-Key": "test-secret-key"},
)
assert response.status_code == 200
def test_health_no_auth_required(self, auth_client):
"""健康检查不需要认证"""
response = auth_client.get("/health")
assert response.status_code == 200
def test_no_env_key_skips_auth(self, test_db, fake_llm, monkeypatch):
"""未配置 API_KEY 时跳过认证"""
monkeypatch.delenv("API_KEY", raising=False)
def override_get_database():
return test_db
app.dependency_overrides[get_database] = override_get_database
with TestClient(app) as c:
response = c.get("/api/sessions")
assert response.status_code == 200
app.dependency_overrides.clear()
关键点:auth_client 不覆盖 verify_api_key------让真实的认证逻辑跑起来。
这是"测试认证"的通用模式:
- 需要测认证 → 不覆盖认证依赖
- 不需要测认证 → 覆盖它(跳过认证)
monkeypatch.setenv 让环境变量在测试内生效,测完自动恢复(Day 19 学的)。
六、跑测试 + 覆盖率
1. 运行
bash
cd day24/ai-assistant-api-v4
pytest tests/api/ -v
输出:
arduino
tests/api/test_health.py::TestHealth::test_health_returns_ok PASSED [ 4%]
tests/api/test_health.py::TestHealth::test_health_has_request_id PASSED [ 8%]
tests/api/test_health.py::TestHealth::test_health_has_process_time PASSED [ 12%]
tests/api/test_chat.py::TestChat::test_chat_creates_new_session PASSED [ 16%]
tests/api/test_chat.py::TestChat::test_chat_continues_session PASSED [ 20%]
tests/api/test_chat.py::TestChat::test_chat_nonexistent_session PASSED [ 24%]
tests/api/test_chat.py::TestChat::test_chat_validation_errors[bad_payload0] PASSED [ 28%]
tests/api/test_chat.py::TestChat::test_chat_validation_errors[bad_payload1] PASSED [ 32%]
...
==================== 23 passed in 2.41s ====================
23 个 API 测试,2.4 秒跑完。
2. 覆盖率
bash
pytest --cov=src/ai_assistant --cov-report=term-missing
输出:
erlang
Name Stmts Miss Cover Missing
-----------------------------------------------------------------------
src/ai_assistant/api/app.py 25 0 100%
src/ai_assistant/api/auth.py 20 2 90% 30-31
src/ai_assistant/api/deps.py 25 2 92%
src/ai_assistant/api/error_handlers.py 40 5 87% 55-59
src/ai_assistant/api/middleware.py 30 4 87% 42-45
src/ai_assistant/api/routes.py 80 8 90%
src/ai_assistant/db_orm.py 50 5 90%
src/ai_assistant/storage_orm.py 70 7 90%
...
-----------------------------------------------------------------------
TOTAL 450 45 90%
90% 覆盖率,相当不错。
未覆盖的部分大多是"异常分支" ------比如连接池爆了、网络错误等极端场景。这些测试成本高、收益低,可以接受。
3. 加一个 pytest 命令
Makefile:
makefile
test-api:
pytest tests/api/ -v
test-all:
pytest --cov=src/ai_assistant --cov-report=term-missing
test-fast:
pytest -m "not integration" -x # -x: 第一个失败就停
用法:
bash
make test-api # 只跑 API 测试
make test-all # 全量 + 覆盖率
make test-fast # 快速模式
七、测试的"陷阱"和"最佳实践"
1. 陷阱:测试污染
症状:单独跑一个测试通过,全量跑失败。
原因: 测试之间共享状态。
常见场景:
python
# ❌ 共享 client,没清理
client = TestClient(app)
def test_a():
client.post("/api/chat", ...) # 造了数据
def test_b():
r = client.get("/api/sessions")
assert r.json()["total"] == 0 # 失败了!因为 test_a 造了数据
修复: 每个测试独立 fixture。
python
# ✅ 每个测试独立数据库
@pytest.fixture
def client(tmp_path):
db = create_db(tmp_path / "test.db")
...
yield client
# 自动清理
今天的 test_db fixture 用了 tmp_path------每个测试的数据库文件不同,天然隔离。
2. 陷阱:慢
症状: 测试跑了几十秒。
原因:
- 真的调 LLM:每次几秒
- 真的建数据库:每次建表
- 没有 Mock
优化:
python
@pytest.fixture(scope="session")
def db_engine():
"""整个测试会话共享一个 engine"""
...
scope="session" 让 fixture 只执行一次------但要注意状态污染。
另一种优化:用 SQLite 内存数据库。
python
@pytest.fixture
def test_db():
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
...
内存数据库不写磁盘,快 10 倍。 缺点是"内存数据库不能跨连接共享",需要 StaticPool。
今天的 fixture 用文件而不是内存 ------为了更接近真实场景,暴露文件权限等问题。生产测试中往往用内存,加快速度。
3. 最佳实践:只测"契约"
API 测试的重点是"契约",不是"实现"。
✅ 该测的:
- 状态码(200 / 404 / 422 / 401)
- 响应结构(字段名、类型)
- 边界(空输入、超长、类型错)
- 认证是否生效
❌ 不该测的:
- 具体的回复内容(模型会变)
- 内部函数调用(那该用单元测试)
- 中间件实现细节(改了就误报)
例子:
python
# ❌ 测了具体回复
def test_chat(client):
r = client.post("/api/chat", json={"message": "你好"})
assert r.json()["reply"] == "你好!我是一个模拟的 AI 助手..." # 换模型就挂
# ✅ 测了结构
def test_chat(client):
r = client.post("/api/chat", json={"message": "你好"})
data = r.json()
assert r.status_code == 200
assert "session_id" in data
assert "reply" in data
assert isinstance(data["reply"], str)
assert len(data["reply"]) > 0 # 不空就行
测"契约",不测"内容"------这样改了实现,测试不用跟着改。
八、前端视角:API 测试就是"Mock Server + 断言"
做过前端的你,对 API 测试应该有点感觉:
前端测试 API 的方式:
- Mock Server(如 MSW):拦截请求,返回假数据
- 对 Mock 断言:验证请求参数、响应结构
FastAPI 测试做的是类似的事:
TestClient(app)≈ MSW 的"拦截器"dependency_overrides≈ 换掉后端实现assert response.status_code≈ 前端expect(res.status).toBe(200)
对照表:
| 前端(Jest + MSW) | FastAPI 测试 |
|---|---|
setupServer(...handlers) |
TestClient(app) |
mockResolvedValue(data) |
dependency_overrides[dep] = fake |
expect(res.status).toBe(200) |
assert response.status_code == 200 |
expect(res.body).toHaveProperty("id") |
assert "id" in response.json() |
expect(fetch).toHaveBeenCalledWith(...) |
assert mock.called_with(...) |
beforeEach / afterEach |
fixture |
核心心智一样: 不测"内部实现",只测"输入 → 输出"。
前端做过的:
js
test("登录接口返回 token", async () => {
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ username: "x", password: "y" }),
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("access_token");
});
后端现在做的:
python
def test_login(client):
response = client.post("/api/login", json={"username": "x", "password": "y"})
assert response.status_code == 200
data = response.json()
assert "access_token" in data
一模一样。
九、今日踩坑
- 忘了
app.dependency_overrides.clear()→ 污染后续测试 TestClient用with语句 → 确保 lifespan 执行(数据库初始化)- 认证接口测不了 → 忘了"不覆盖认证依赖",用了默认 client
TestClient用 async 函数报错 →TestClient是同步的,要么用同步函数,要么用AsyncClienttest_db用了内存数据库但忘了StaticPool→ 连接不通,报错- 断言具体回复内容 → 模型一改就挂,测结构就好
- 忘了测边界 → 只测了 happy path,错误码场景没测
- 每个测试都重建数据库 → 慢,能共享就
scope="session" - 测试写了
time.sleep→ 慢,用 Mock 替换 - 测试顺序依赖 → 一定要能独立跑(
pytest tests/api/test_chat.py::TestChat::test_chat_creates_new_session) - 忘了测"未认证访问" → 认证的漏洞就在这里
response.json()前没检查状态码 → 500 返回 HTML,json()报错,看不出真正问题
十、今日总结
今天核心收获三件事:
- FastAPI 测试三件套 :
TestClient+dependency_overrides+ 临时数据库,一切可测 - API 测试测"契约":状态码、响应结构、边界,不测"具体内容"
- 认证测试要用"真认证 client":不覆盖认证依赖,才能真正测到
一个感受:
写业务代码时你会觉得"测试好麻烦",但写 API 时你会发现------没有测试更麻烦。
- 前端说"接口变了" → 你得对一遍 Postman
- 部署后发现 500 → 你只能翻日志一个个查
- 重构存储层 → 完全不知道有没有破坏 API
API 测试的价值,就是"改了任何东西,一条命令告诉你有没有搞坏。"
你的 API 现在有 90% 覆盖率保障。 后面 Day 46 换真 LLM、Day 81 加 Agent 工具调用,只要这套测试不挂,就说明没破坏现有功能------这就是"信心"。
Day 24 完成度:✅
明日预告(Day 25)
- API 性能优化 :
- 缓存:内存缓存 / Redis 缓存
- 分页:大数据量分页策略
- 异步优化:并发处理多请求
- N+1 查询问题排查
- 给 API 加一个缓存层
- 用
ab/hey压测对比优化前后
最后
如果这篇对你有用,点个赞让我知道有人在看。
评论区互动: 你写过后端测试吗?用的是什么?和前端测试比,你觉得哪个更麻烦?
有问题评论区见,我们第 25 天继续。
100 天,第 25 天见。
附录:系列目录
已完成 ✅
| 阶段 | 天数 | 内容 |
|---|---|---|
| 基础 | 01-06 | Python 语法、数据结构、函数、OOP、文件、依赖管理 |
| 网络 | 07-08 | HTTP 请求、异步 asyncio |
| 工程 | 09-12 | 正则、装饰器、Pydantic、日志调试 |
| 项目 | 13-14 | 命令行 AI 助手 v1 / v2 |
| 复盘 | 15 | 阶段总结 |
| 数据 | 16-17 | SQLite 数据库、SQLAlchemy ORM |
| 测试 | 18-19 | pytest 入门 + 进阶 |
| 工具 | 20 | CLI 打包与发布 |
| 后端 | 21-24 | FastAPI 入门 / 进阶 / 数据库整合 / API 测试 ✅ |
进行中 🎯
| 阶段 | 天数 | 内容 |
|---|---|---|
| 后端 | 25-30 | 性能优化 + 部署上线 |
| 大模型 | 31-45 | LLM API + Prompt 工程 |
| RAG | 46-60 | 向量数据库 + 知识库 |
| Agent | 61-80 | LangChain / 工具调用 |
| 实战 | 81-100 | 完整 Agent 项目上线 |
100 天后见,一起从切图仔变成 Agent 开发者。