Files
ai-agent-dev/27_部署与评估.ipynb
2026-07-08 10:09:42 +08:00

688 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 27 部署与评估\n",
"\n",
"## 学习目标\n",
"1. 了解智能体应用的部署方式,例如本地函数、本地 API、Docker 和云平台\n",
"2. 掌握智能体效果评估的常见指标和方法\n",
"3. 学会用测试集评估智能体的稳定性和准确性\n",
"4. 理解日志、追踪和 LangSmith 在智能体项目中的作用\n",
"5. 能够为一个智能体项目设计简单的上线检查流程\n",
"\n",
"前面我们已经完成了一个带交互界面的智能体项目。本节课要解决的问题是:**项目写完以后,怎么交付给别人使用?怎么判断它运行得好不好?**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 为什么要学习部署与评估\n",
"\n",
"很多初学者做智能体项目时,会停留在 notebook 能运行的阶段。\n",
"\n",
"但真实项目还需要回答三个问题:\n",
"\n",
"1. **别人怎么使用它?**\n",
"2. **它回答得好不好?**\n",
"3. **出问题时怎么排查?**\n",
"\n",
"这三个问题分别对应:\n",
"\n",
"| 问题 | 对应能力 |\n",
"| --- | --- |\n",
"| 怎么使用 | 部署 |\n",
"| 好不好用 | 评估 |\n",
"| 怎么排查 | 追踪与日志 |\n",
"\n",
"所以,智能体开发不是写完代码就结束。真正完整的流程应该是:\n",
"\n",
"```text\n",
"开发 -> 测试 -> 评估 -> 部署 -> 监控 -> 迭代\n",
"```\n",
"\n",
"本节会用通俗方式把这条链路讲清楚。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 智能体常见部署方式\n",
"\n",
"部署可以理解为:**把只能自己运行的代码,变成别人也能使用的服务。**\n",
"\n",
"常见部署方式有下面几种:\n",
"\n",
"| 部署方式 | 适合场景 | 特点 |\n",
"| --- | --- | --- |\n",
"| 本地函数 | 学习、测试、内部脚本 | 最简单,但只能在代码里调用 |\n",
"| 命令行程序 | 工具型项目、个人使用 | 可以通过终端交互 |\n",
"| 本地 API | 前后端分离、系统集成 | 其他程序可以通过 HTTP 调用 |\n",
"| Docker | 统一环境、方便迁移 | 避免“我电脑能跑,你电脑不能跑” |\n",
"| 云平台 | 正式上线、多用户访问 | 稳定性更高,但配置更复杂 |\n",
"\n",
"初学时不要一上来就追求复杂部署。\n",
"\n",
"推荐顺序是:\n",
"\n",
"```text\n",
"函数调用 -> 命令行 -> 本地 API -> Docker -> 云平台\n",
"```\n",
"\n",
"每一步都是在上一层基础上增加一点工程化能力。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 准备一个可部署的智能体核心函数\n",
"\n",
"部署前最重要的一件事是:**把核心逻辑封装成一个清晰的函数。**\n",
"\n",
"不管后面是接命令行、Web API还是云服务最终都应该调用同一个核心函数。\n",
"\n",
"下面我们用一个简化版学习助手作为示例。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def run_learning_assistant(user_input):\n",
" if '计划' in user_input or '安排' in user_input:\n",
" intent = 'plan'\n",
" response = '建议先学习 State、Node、Edge再学习条件边、工具调用和项目实战。'\n",
" elif '解释' in user_input or '什么是' in user_input:\n",
" intent = 'explain'\n",
" response = 'LangGraph 可以理解为用图结构组织智能体流程的框架。'\n",
" else:\n",
" intent = 'chat'\n",
" response = '我可以帮你制定学习计划、解释概念或梳理项目思路。'\n",
"\n",
" return {\n",
" 'input': user_input,\n",
" 'intent': intent,\n",
" 'response': response\n",
" }\n",
"\n",
"result = run_learning_assistant('帮我制定 LangGraph 学习计划')\n",
"print(result)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这段代码是一个最小可部署单元。\n",
"\n",
"#### `run_learning_assistant(user_input)`\n",
"这个函数接收用户输入,然后返回一个结构化结果。\n",
"\n",
"它没有直接使用 `input()`,也没有直接打印最终答案。\n",
"\n",
"这样做是为了方便部署。因为不同界面拿到结果后,可以用自己的方式展示:\n",
"\n",
"- 命令行可以 `print`\n",
"- Web API 可以返回 JSON\n",
"- 前端页面可以渲染到聊天窗口\n",
"\n",
"#### 为什么返回字典\n",
"函数返回:\n",
"\n",
"```python\n",
"{\n",
" 'input': user_input,\n",
" 'intent': intent,\n",
" 'response': response\n",
"}\n",
"```\n",
"\n",
"这比只返回一个字符串更适合工程项目。\n",
"\n",
"因为调用方不仅能拿到回复,还能看到系统识别出的意图,方便调试和评估。\n",
"\n",
"#### 部署前的关键原则\n",
"无论项目内部多复杂,都建议对外提供一个简单入口。\n",
"\n",
"可以把它理解成智能体项目的“总开关”。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 本地 API 部署的基本思路\n",
"\n",
"API 部署的意思是:让其他程序通过 HTTP 请求调用你的智能体。\n",
"\n",
"例如前端页面可以发送:\n",
"\n",
"```text\n",
"POST /chat\n",
"{\"message\": \"帮我制定学习计划\"}\n",
"```\n",
"\n",
"服务端返回:\n",
"\n",
"```text\n",
"{\"response\": \"建议先学习 State、Node、Edge...\"}\n",
"```\n",
"\n",
"下面先不用 FastAPI直接用一个普通函数模拟 API 处理过程。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def chat_api_handler(request_json):\n",
" user_message = request_json.get('message', '')\n",
"\n",
" if not user_message.strip():\n",
" return {\n",
" 'ok': False,\n",
" 'error': 'message 不能为空'\n",
" }\n",
"\n",
" result = run_learning_assistant(user_message)\n",
"\n",
" return {\n",
" 'ok': True,\n",
" 'data': result\n",
" }\n",
"\n",
"request = {'message': '解释一下什么是 LangGraph'}\n",
"response = chat_api_handler(request)\n",
"print(response)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这段代码模拟了一个 API 接口。\n",
"\n",
"#### `request_json`\n",
"它代表外部系统传来的请求数据。\n",
"\n",
"真实 API 中,这个数据通常来自 HTTP 请求体。\n",
"\n",
"#### `request_json.get('message', '')`\n",
"这行代码从请求中取出用户消息。\n",
"\n",
"如果请求里没有 `message`,就默认使用空字符串。\n",
"\n",
"#### 空输入检查\n",
"如果用户没有传有效内容,接口返回:\n",
"\n",
"```python\n",
"{'ok': False, 'error': 'message 不能为空'}\n",
"```\n",
"\n",
"这是部署时常见的边界检查。\n",
"\n",
"#### 调用核心函数\n",
"真正处理智能体逻辑的是:\n",
"\n",
"```python\n",
"result = run_learning_assistant(user_message)\n",
"```\n",
"\n",
"API 层不应该写太多智能体逻辑,它只负责接收请求、调用核心函数、返回结果。\n",
"\n",
"#### 为什么这样分层\n",
"分层后,项目会更容易维护:\n",
"\n",
"- 智能体逻辑改动,不影响 API 格式\n",
"- API 部署方式改动,不影响智能体核心\n",
"- 测试时可以单独测试核心函数,也可以测试接口函数"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. FastAPI 部署示意\n",
"\n",
"真实项目中,经常会用 FastAPI 把智能体包装成 HTTP 服务。\n",
"\n",
"下面是示意代码。这里放在 Markdown 中展示,不要求你现在必须运行。\n",
"\n",
"```python\n",
"from fastapi import FastAPI\n",
"from pydantic import BaseModel\n",
"\n",
"app = FastAPI()\n",
"\n",
"class ChatRequest(BaseModel):\n",
" message: str\n",
"\n",
"@app.post('/chat')\n",
"def chat(request: ChatRequest):\n",
" result = run_learning_assistant(request.message)\n",
" return result\n",
"```\n",
"\n",
"如果保存为 `app.py`,通常可以用下面命令启动:\n",
"\n",
"```bash\n",
"uvicorn app:app --host 0.0.0.0 --port 8000\n",
"```\n",
"\n",
"启动后,其他程序就可以通过接口调用智能体。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. Docker 和云平台部署怎么理解\n",
"\n",
"当项目只在自己电脑上运行时,问题通常不大。\n",
"\n",
"但交给别人运行时,经常会出现:\n",
"\n",
"- Python 版本不同\n",
"- 依赖包没安装\n",
"- 环境变量没配置\n",
"- 系统路径不一致\n",
"\n",
"Docker 的作用就是把运行环境打包起来。\n",
"\n",
"可以把 Docker 镜像理解成一个“装好环境的盒子”。别人只要运行这个盒子,就能得到接近一致的环境。\n",
"\n",
"云平台部署则是在远程服务器上运行这个应用,让更多用户可以访问。\n",
"\n",
"一个常见上线链路是:\n",
"\n",
"```text\n",
"本地开发 -> 写 requirements.txt -> 写 Dockerfile -> 构建镜像 -> 部署到云服务器\n",
"```\n",
"\n",
"本课程重点是智能体开发,所以这里只需要理解部署思路,不要求一次掌握所有运维细节。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. 为什么要评估智能体\n",
"\n",
"部署解决的是“能不能给别人用”。\n",
"\n",
"评估解决的是“用得好不好”。\n",
"\n",
"智能体评估通常关注:\n",
"\n",
"| 指标 | 含义 | 示例 |\n",
"| --- | --- | --- |\n",
"| 正确性 | 回答是否正确 | 是否解释对了概念 |\n",
"| 相关性 | 是否回答了用户问题 | 问学习计划,不要回复天气 |\n",
"| 完整性 | 回答是否覆盖关键点 | 学习计划是否有步骤 |\n",
"| 稳定性 | 多次运行是否表现一致 | 同类问题是否走同类分支 |\n",
"| 成本 | 调用模型的费用和耗时 | token、延迟、API 成本 |\n",
"| 安全性 | 是否避免危险输出 | 不泄露密钥,不执行高风险操作 |\n",
"\n",
"初学阶段可以先做最简单的评估:准备一批测试问题,看系统是否识别出正确意图。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8. 构建一个简单评估集\n",
"\n",
"评估集就是提前准备好的测试样例。\n",
"\n",
"每条样例通常包含:\n",
"\n",
"- 输入问题\n",
"- 期望结果\n",
"\n",
"下面我们先评估意图识别是否正确。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"eval_dataset = [\n",
" {'input': '帮我制定 LangGraph 学习计划', 'expected_intent': 'plan'},\n",
" {'input': '请安排一个 AI 学习路线', 'expected_intent': 'plan'},\n",
" {'input': '解释一下什么是条件边', 'expected_intent': 'explain'},\n",
" {'input': '什么是智能体状态', 'expected_intent': 'explain'},\n",
" {'input': '你好,你能做什么', 'expected_intent': 'chat'},\n",
"]\n",
"\n",
"print('评估集数量:', len(eval_dataset))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这段代码定义了一个小型评估集。\n",
"\n",
"#### `eval_dataset`\n",
"它是一个列表,里面每个元素都是一个测试样例。\n",
"\n",
"每个样例有两个字段:\n",
"\n",
"- `input`:用户输入\n",
"- `expected_intent`:期望识别出的意图\n",
"\n",
"#### 为什么先评估意图\n",
"因为当前学习助手的核心流程是:\n",
"\n",
"```text\n",
"输入 -> 意图识别 -> 分支处理 -> 回复\n",
"```\n",
"\n",
"如果意图识别错了,后面的节点也会走错。\n",
"\n",
"所以意图识别是一个很适合入门评估的指标。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 9. 自动运行评估\n",
"\n",
"有了评估集后,就可以写代码自动跑一遍,并计算准确率。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def evaluate_intent(dataset):\n",
" records = []\n",
" correct_count = 0\n",
"\n",
" for item in dataset:\n",
" result = run_learning_assistant(item['input'])\n",
" predicted = result['intent']\n",
" expected = item['expected_intent']\n",
" is_correct = predicted == expected\n",
"\n",
" if is_correct:\n",
" correct_count += 1\n",
"\n",
" records.append({\n",
" 'input': item['input'],\n",
" 'expected': expected,\n",
" 'predicted': predicted,\n",
" 'correct': is_correct\n",
" })\n",
"\n",
" accuracy = correct_count / len(dataset)\n",
" return accuracy, records\n",
"\n",
"accuracy, records = evaluate_intent(eval_dataset)\n",
"print('准确率:', accuracy)\n",
"for record in records:\n",
" print(record)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这段代码完成了一个最小自动评估流程。\n",
"\n",
"#### `evaluate_intent(dataset)`\n",
"这个函数接收评估集,然后逐条测试。\n",
"\n",
"#### `result = run_learning_assistant(item['input'])`\n",
"每条测试样例都会调用一次智能体核心函数。\n",
"\n",
"这和真实用户调用项目的方式是一致的。\n",
"\n",
"#### `predicted` 和 `expected`\n",
"- `predicted` 是系统实际识别出的意图\n",
"- `expected` 是我们提前标注的正确意图\n",
"\n",
"如果两者相等,就认为这条样例通过。\n",
"\n",
"#### `accuracy`\n",
"准确率计算公式是:\n",
"\n",
"```text\n",
"准确率 = 正确数量 / 总样例数量\n",
"```\n",
"\n",
"#### `records`\n",
"除了给出总分,还要保存每条样例的详细结果。\n",
"\n",
"因为只有总分不够。我们还需要知道到底哪些问题错了,才能继续优化。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 10. 记录运行日志\n",
"\n",
"部署后,用户的问题可能非常多。\n",
"\n",
"如果没有日志,出问题时就很难排查。\n",
"\n",
"日志至少应该记录:\n",
"\n",
"- 用户输入\n",
"- 系统输出\n",
"- 识别出的意图\n",
"- 是否成功\n",
"- 运行时间\n",
"\n",
"下面用一个简单包装函数演示如何记录日志。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"run_logs = []\n",
"\n",
"def run_with_trace(user_input):\n",
" start_time = time.time()\n",
"\n",
" try:\n",
" result = run_learning_assistant(user_input)\n",
" success = True\n",
" error = ''\n",
" except Exception as exc:\n",
" result = {'input': user_input, 'intent': '', 'response': ''}\n",
" success = False\n",
" error = str(exc)\n",
"\n",
" latency = time.time() - start_time\n",
"\n",
" log = {\n",
" 'input': user_input,\n",
" 'intent': result['intent'],\n",
" 'response': result['response'],\n",
" 'success': success,\n",
" 'error': error,\n",
" 'latency_seconds': round(latency, 4)\n",
" }\n",
"\n",
" run_logs.append(log)\n",
" return result\n",
"\n",
"run_with_trace('解释一下什么是 LangGraph')\n",
"run_with_trace('帮我安排学习计划')\n",
"\n",
"for log in run_logs:\n",
" print(log)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这段代码演示了最基础的追踪思路。\n",
"\n",
"#### `run_logs`\n",
"这是一个列表,用来保存运行日志。\n",
"\n",
"真实项目中,日志通常会写入文件、数据库或日志平台。\n",
"\n",
"#### `start_time = time.time()`\n",
"记录开始时间,用来计算本次调用耗时。\n",
"\n",
"#### `try ... except ...`\n",
"部署后的系统不能因为一次错误就直接崩溃。\n",
"\n",
"所以这里用 `try ... except` 捕获异常,并把错误信息写入日志。\n",
"\n",
"#### `latency_seconds`\n",
"表示运行耗时。\n",
"\n",
"对于真实大模型应用,耗时是非常重要的指标。\n",
"\n",
"用户不仅关心回答对不对,也关心等多久。\n",
"\n",
"#### 追踪的价值\n",
"日志可以帮助我们回答:\n",
"\n",
"- 用户都问了什么?\n",
"- 哪些问题经常失败?\n",
"- 哪类意图识别不准?\n",
"- 哪些请求耗时很长?\n",
"\n",
"这些信息是后续优化智能体的重要依据。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 11. LangSmith 是做什么的\n",
"\n",
"LangSmith 是 LangChain 生态中的调试、追踪和评估平台。\n",
"\n",
"你可以把它理解成智能体应用的“运行记录仪”。\n",
"\n",
"它可以帮助你查看:\n",
"\n",
"- 每次调用输入了什么\n",
"- 调用了哪些链、工具或模型\n",
"- 每一步输出是什么\n",
"- 哪一步出错\n",
"- 每次调用耗时多久\n",
"- token 和成本情况\n",
"\n",
"本节我们不强制接入 LangSmith因为它需要账号和环境变量配置。\n",
"\n",
"但你需要理解它解决的问题:**让智能体运行过程可观察、可追踪、可评估。**\n",
"\n",
"在真实项目中,如果智能体流程比较复杂,建议尽早接入类似追踪工具。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 12. 上线前检查清单\n",
"\n",
"智能体项目上线前,建议检查下面这些内容。\n",
"\n",
"| 检查项 | 说明 |\n",
"| --- | --- |\n",
"| 环境变量 | API Key、Base URL 是否配置正确 |\n",
"| 依赖版本 | requirements 是否完整 |\n",
"| 基础测试 | 核心函数是否能正常运行 |\n",
"| 评估集 | 常见问题是否通过测试 |\n",
"| 错误处理 | 空输入、异常情况是否能返回友好提示 |\n",
"| 日志追踪 | 是否能记录输入、输出和错误 |\n",
"| 安全检查 | 是否避免泄露密钥和执行危险操作 |\n",
"| 成本控制 | 是否限制过长输入和频繁调用 |\n",
"\n",
"这个清单不复杂,但很实用。\n",
"\n",
"它能帮助你从“代码能跑”过渡到“项目能交付”。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 13. 本节小结\n",
"\n",
"本节最重要的内容有五点:\n",
"\n",
"1. **部署是把智能体从个人代码变成别人可使用的服务**\n",
"2. **核心逻辑应该先封装成稳定函数再接命令行、API 或 Web 界面**\n",
"3. **评估集可以帮助我们判断智能体是否稳定、准确**\n",
"4. **日志和追踪可以帮助我们排查问题、分析用户行为、持续优化系统**\n",
"5. **LangSmith 这类工具可以让复杂智能体流程变得可观察、可评估**\n",
"\n",
"到这里,一个智能体项目就不只是能运行的 demo而是开始具备工程化交付能力。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 14. 本节练习\n",
"\n",
"1. 给 `eval_dataset` 增加 5 条新的测试样例\n",
"2. 修改 `run_learning_assistant`,增加一个 `summary` 意图\n",
"3. 修改评估函数,同时评估 `intent` 和 `response` 中是否包含关键字\n",
"4. 给 `run_with_trace` 增加一个 `timestamp` 字段,记录调用时间\n",
"5. 思考:如果这个助手要部署给 100 个用户使用,还需要补充哪些能力?"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}