内容
# OpenAgent 演示
一个用于“企业AI智能体”闪电演讲的独立模拟:一个面向客户的聊天机器人,搜索餐厅并通过真正的MCP工具预订餐桌,背后是一个模拟的“Elasticsearch”(SQLite)餐厅索引。
```
浏览器(web/index.html)
│ 获取 POST /chat
▼
FastAPI 聊天机器人微服务(chat_service/)
│ Claude 工具使用循环(Anthropic Messages API)
│ MCP 客户端,stdio 传输
▼
MCP 服务器(mcp_server/server.py)
│ 读取工具:search_restaurants, get_availability
│ 写入工具:create_booking, cancel_booking
▼
SQLite “餐厅索引”(data/restaurant_booking.db)
```
读写工具的分离是刻意的,而不是仅仅是外观:读取工具对于智能体来说是安全的,可以随意调用,而在推理过程中,写入工具则需要系统要求智能体重新陈述细节并在调用前获得明确的用户确认。这就是与完整的“在生产中使智能体可靠”的课程的关联 - 这个存储库是它的免费闪电课程预告。
## 智能体承载
“承载”是指围绕模型调用的代码 - 将单个LLM请求转换为可以行动的智能体的所有内容。Claude本身只是将一个输入映射到一个输出;它不会调用工具、检查结果或自行决定重试。承载正是闭合了这个循环。去掉承载,你就得到了一个普通的聊天机器人:一个消息输入,一个回复输出,无法实际查找任何内容或执行任何操作。添加承载,同一个模型可以搜索数据库,注意到它需要更多信息,并采取行动 - 这就是在这个存储库中“聊天机器人”和“智能体”之间的全部区别(参见本项目历史早期关于“聊天机器人与智能体”的讨论:UI是一个聊天机器人,`chat_service/main.py`正是使其背后的内容成为智能体)。
具体来说,这里的承载是`chat_service/main.py`的`/chat`处理程序中的`while True`循环:
1. **将客人的消息**追加到该会话的对话历史中。
2. **用Claude**调用完整的历史记录和从MCP服务器获取的工具模式(`anthropic_client.messages.create(..., tools=tools, messages=history)`)。
3. **检查`response.stop_reason`。** 如果它不是`"tool_use"`,Claude给出了最终的文本答案 - 将其返回给客人,循环结束。
4. **如果它是`"tool_use"`,**Claude的响应包含一个或多个`tool_use`块(工具名称 + 参数,它想要调用)。承载不会询问读取工具的权限 - 它只是为每个块调用`mcp_client.call_tool(block.name, block.input)`,通过真正的MCP协议,并获得结果。
5. **将工具结果**追加到对话中,作为`tool_result`消息,然后**返回步骤 2** - Claude看到结果并决定下一步:调用另一个工具或现在回答。
这个循环就是整个承载。存储库中的其他所有内容 - MCP工具定义、读写分离、系统提示中的“确认后再预订”规则 - 都是承载的输入(工具模式、系统提示)或不变式,承载的工具调用步骤应该尊重这些输入,而不是独立的机制。它也是代码库中模型决策和工具调用都经过的唯一位置,这就是为什么它是附加生产可靠性问题的自然位置,而承载在这里故意省略了这些问题:写入的确认门控已经完成(在提示中),但幂等键、重试、超时、速率限制和审计日志都将围绕这个相同的循环进行,而不是生活在其他地方。

## MCP 工具与数据
承载图将“调用MCP工具”视为一个盒子;本节将深入探讨该调用的另一侧 - `mcp_server/server.py`实际暴露的四个工具,以及每个工具访问的模拟“Elasticsearch”索引(`data/restaurant_booking.db`)中的哪些表。
读写分离不仅仅是命名约定 - 它是承载的确认规则所依赖的内容。`search_restaurants`和`get_availability`仅运行`SELECT`,因此承载允许Claude在仍在确定客人需要什么时随意调用它们。`create_booking`和`cancel_booking`修改状态,因此系统要求在承载调用它们之前获得明确的客人确认。
注意,两个写入工具都恰好触及**两个**表 - `create_booking`同时插入一个新的`bookings`行*和*从`availability`中扣除已使用的座位;`cancel_booking`同时将`bookings`行标记为已取消*和*将这些座位释放回可用状态。这种配对必须一起发生,否则数据将变得不一致(已取消的预订未释放其座位,或“已确认”的预订重复预订了一张桌子)- 这是一个小而具体的例子,说明为什么写入工具在生产中比读取工具需要更多的关注,而不仅仅是一个确认步骤。

## 可观察性(Langfuse)
`chat_service/main.py`配备了 [Langfuse](https://langfuse.com) (真实的SDK,而不是模拟器),您可以实时监控上述承载循环在演示期间的实际执行情况,调用情况。
**会话分组。** 在此应用中,会话只是浏览器页面加载(参见 `web/chat.js` - `sessionId = crypto.randomUUID()`,每次刷新时都会生成一个新的,从未持久化)。每个 `/chat` 调用都是其自己的 Langfuse 跟踪,但所有共享相同 `session_id` 的跟踪都分组到一个 Langfuse 会话中(通过 `propagate_attributes(session_id=...)`),因此整个多轮对话在仪表板中显示为单个会话,而不是分散的、断开的跟踪 - 刷新页面后,您将开始一个新的会话,与对话本身相同。
**每轮捕获的内容:**
- 根跨度(`chat_turn`),用于整个请求,输入 = 客人消息,输出 = 最终回复
- 每个Claude API调用的`generation`观察(模型,完整的消息历史记录,内容块输出,令牌使用)- 单轮可以有多个,如果Claude在回答之前调用工具
- 每个MCP工具调用的`tool`观察(参数输入,结果输出)
聊天界面还为每个回复提供了 **“在 Langfuse 中查看此轮的跟踪”** 链接(`data.trace_url`来自 `langfuse.get_trace_url()`),您可以直接从实时演示跳转到屏幕上的确切跟踪。
将您的项目的密钥添加到 `chat_service/.env` 以启用此功能(与 `ANTHROPIC_API_KEY` 相同的文件):
```
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com # 如果使用 Langfuse Cloud(默认),则省略
```
## 真实与模拟
- **真实**:Claude 工具使用循环,实际 MCP 协议(官方 `mcp` SDK,stdio 传输),FastAPI 微服务边界,HTML/JS 前端,Langfuse 可观察性。
- **模拟**:SQLite 代替 Elasticsearch/预订数据库 - 交换 `mcp_server/server.py` 的 `_connect()`/SQL 为真实的 ES + Postgres 调用,而无需更改工具接口或其上游的任何内容。会话历史记录在内存中(演示足够好,但不适用于生产环境)。
## 设置
```bash
uv venv --python 3.11 .venv
uv pip install --python .venv mcp anthropic fastapi "uvicorn[standard]" python-dotenv langfuse
uv pip install --python .venv -r evals/requirements.txt # 仅在运行评估时需要
.venv/bin/python data/seed_db.py # (重新)构建模拟餐厅数据库
export ANTHROPIC_API_KEY=sk-...
```
## 运行(3 个终端)
```bash
# 1. 聊天机器人微服务(这也会将 MCP 服务器作为子进程启动)
cd chat_service && ../.venv/bin/uvicorn main:app --port 8000 --reload
# 2. 静态网站
cd web && python3 -m http.server 5500
# 3. 打开网站
open http://localhost:5500
```
点击右下角的 💬 按钮,打开礼宾聊天。
为了单独检查MCP服务器(不涉及LLM):
```bash
.venv/bin/python mcp_server/server.py # 应该等待stdio — Ctrl+C退出
```
## 评估
`evals/` 是一个小型评估驱动开发的承载:25个手工编写的案例(搜索、可用性、预订、预订边缘情况、安全/鲁棒性),通过两种独立的方式检查 - 确定性检查智能体应该/不应该进行的MCP工具调用的确切序列,以及LLM作为裁判检查实际对话是否满足纯英文标准。案例只有在两者都同意时才通过。参见 `evals/cases.csv` 获取完整的基准。
**先决条件:** 聊天服务必须已经运行(参见上面的运行步骤 1 - 承载驱动它通过真正的HTTP,相同的路径前端使用):
```bash
# 运行整个套件(在每个案例之前重置数据库到其原始的种子状态,以便案例彼此独立)
.venv/bin/python evals/run_evals.py
# 只运行一个案例进行迭代
.venv/bin/python evals/run_evals.py --case-id booking-001
```
这会为每个案例打印通过/失败行,并写入 `evals/results.csv`,其中包含基准和实际结果并排每行(预期工具调用/对话标准与实际工具跟踪/回复/裁判推理),因此失败很容易在不深入日志的情况下检查。
注意:这会为每个案例重新运行智能体(和裁判)实时对抗API,因此会产生真正的API调用,并需要几分钟才能完成所有25个 - 不要比需要的更频繁地运行;使用 `--case-id`针对您正在处理的案例。
## 存储库布局
```
docs/agent-harness.svg # 工具使用循环图,嵌入上方
docs/mcp-tools-data.svg # 4个MCP工具 + 数据表图
data/seed_db.py # 构建模拟餐厅索引 + 种子数据
mcp_server/server.py # MCP 服务器:搜索/读取 + 预订/写入工具
chat_service/main.py # FastAPI + Claude 工具使用循环 + MCP 客户端
chat_service/mcp_client.py
web/index.html # OpenAgent 风格的着陆页
web/style.css
web/chat.js # 聊天小部件连接到 POST /chat
web/images/ # 真实的食品照片,每个餐厅一个(参见下面的注释)
evals/cases.csv # 25 个评估案例:输入 + 双部分基准
evals/matcher.py # 确定性工具调用序列检查器
evals/judge.py # LLM 作为裁判,用于对话标准
evals/run_evals.py # 承载:重置 DB,驱动聊天服务,写入 results.csv
evals/results.csv # 最新运行的输出(生成,非手动编辑)
```
`web/images/`中的餐厅照片是免费许可(公共领域/CC)的照片,通过Wikimedia Commons的搜索API从Wikimedia Commons拉取,而不是任何真实餐厅连锁店的库存照片 - 每个餐厅都挑选并筛选,以避免在拍摄时可见的任何竞争对手品牌(一个牛排照片被拒绝的原因正是如此)。