阶段4(上):LangGraph、Function Calling、MCP(SDK 2.x)、Agent 评测与运维、Agent 设计专题;run_examples 设置 RUN_EXAMPLES=1
This commit is contained in:
1 parent
6650f7eb6d
commit
caaec830b3
38 files changed
+3515
-64
No files matched your search
@@ -0,0 +1,87 @@
|
||||
---
|
||||
id: ai.agent-design.scenario.report-agent
|
||||
title: 运营报表 Agent:规划、只读 SQL 工具、结构化输出和任务进度
|
||||
type: scenario
|
||||
domain: ai
|
||||
stack: [agent-design, langgraph, postgresql, fastapi]
|
||||
tags: [报表 Agent, Text-to-SQL, 规划, 结构化输出, 任务状态, 流式进度]
|
||||
level: advanced
|
||||
prerequisites: [ai.agent-design.react-planning]
|
||||
est_hours: 3
|
||||
related: [ai.agent-design.interview, ai.langgraph.persistence-hitl, ai.agent-ops.observability-cost]
|
||||
week: [11, 13]
|
||||
---
|
||||
|
||||
# 运营报表 Agent:规划、只读 SQL 工具、结构化输出和任务进度
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
运营每周要找数据组要报表(“9 月各大区 GMV 和环比”),排队要两三天。希望做一个报表 Agent:用自然语言提需求,自动查数、出图、写结论。原型的问题:
|
||||
|
||||
1. 生成的 SQL 偶尔写错表名,或者把“退款订单”也算进了 GMV;
|
||||
2. 有一次生成了没有时间条件的查询,扫描了整张订单表,拖慢了业务库;
|
||||
3. 复杂报表要跑一两分钟,页面一直转圈,用户以为卡死了;
|
||||
4. 结论里的数字和图表对不上。
|
||||
|
||||
## 约束
|
||||
|
||||
- 只能读,不能写;不能影响线上业务库。
|
||||
- 指标口径(GMV、退款率)以数据组的定义为准。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **工作流 + 局部 Agent**:整体流程固定为“理解需求 → 规划 → 查询 → 出图 → 写结论”,只在“规划和查询”环节让模型多步探索。
|
||||
2. **指标口径作为 Skill**:把指标定义、表结构、示例 SQL 写成技能文件,按需加载,而不是让模型猜。
|
||||
3. **SQL 工具的安全边界**:连接只读从库 / 数仓;只允许 SELECT(用 SQL 解析器校验,不靠字符串匹配);强制时间范围、`LIMIT`、语句超时(`statement_timeout`);先 `EXPLAIN` 检查预估行数,超过阈值拒绝。
|
||||
4. **结构化输出**:结论用 Schema 输出(指标名、数值、来源查询 ID),数值必须来自查询结果——生成后程序校验每个数字都能在结果中找到。
|
||||
5. **任务状态和进度**:任务表记录状态(排队 / 规划中 / 查询中 / 生成中 / 完成 / 失败),LangGraph 节点用 `custom` 流推送进度,前端通过 SSE 显示;失败的步骤可以重试,不用从头开始。
|
||||
|
||||
## 关键代码
|
||||
|
||||
只读 SQL 工具的校验(使用 `sqlglot` 解析 SQL,示意,API 以官方文档为准):
|
||||
|
||||
```python
|
||||
import sqlglot
|
||||
from sqlglot import exp
|
||||
|
||||
|
||||
def validate_sql(sql: str) -> str:
|
||||
tree = sqlglot.parse_one(sql, read="postgres")
|
||||
if not isinstance(tree, exp.Select):
|
||||
raise ValueError("只允许 SELECT")
|
||||
tables = {t.name for t in tree.find_all(exp.Table)}
|
||||
if not tables <= ALLOWED_TABLES:
|
||||
raise ValueError(f"不允许访问的表:{tables - ALLOWED_TABLES}")
|
||||
if "created_at" not in sql:
|
||||
raise ValueError("必须带时间范围条件")
|
||||
if not tree.args.get("limit"):
|
||||
tree = tree.limit(1000)
|
||||
return tree.sql(dialect="postgres")
|
||||
|
||||
|
||||
async def run_query(sql: str) -> list[dict]:
|
||||
sql = validate_sql(sql)
|
||||
async with readonly_pool.connection() as conn:
|
||||
await conn.execute("SET statement_timeout = '15s'")
|
||||
return await (await conn.execute(sql)).fetchall()
|
||||
```
|
||||
|
||||
## 取舍
|
||||
|
||||
| 选择 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| **工作流 + 局部 Agent(本方案)** | 可控、可展示进度、容易评测 | 灵活性有限 |
|
||||
| 完全自主的 Agent | 能处理开放问题 | 难控制成本和正确性 |
|
||||
| 预定义指标 API(语义层),不让模型写 SQL | 口径一致、最安全 | 需要先建设指标体系,覆盖不到的问题答不了 |
|
||||
| 模型直接写 SQL | 覆盖面广 | 口径和安全风险,需要本方案中的护栏 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 只用字符串判断 `SELECT`,被 `WITH ... DELETE` 或多语句绕过:用解析器,并且数据库账号本身只读。
|
||||
- 连接业务主库查询。
|
||||
- 结论中的数字由模型“复述”,不校验来源。
|
||||
- 长任务没有进度和超时,用户反复点击生成多个重复任务(要防重复提交)。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “报表 Agent 我没有做成完全自主的,而是固定流程,只在规划和查询环节让模型多步探索。指标口径写成 Skill 按需加载;SQL 工具连只读从库,用解析器只允许 SELECT 和白名单表,强制时间范围、LIMIT 和语句超时;结论用结构化输出,每个数字都要能在查询结果里找到;任务有明确的状态,进度通过 SSE 推给前端,失败的步骤可以单独重试。”
|
||||
Reference in new issue
Block a user