002|设计一个 Agent 的六要素:从需求到系统架构
Agent 不是 prompt 越复杂越好,而是要把角色、目标、工具、步骤、约束、输出格式这六件事说清楚。
核心结论:一个好的 Agent Prompt 不是"写得长",而是把角色、目标、工具、步骤、约束、输出格式这六件事说清。一个复杂的 Agent 系统,还需要把不同性质的能力拆成多个子 Agent,并通过调度器协调。
一、从一个常见误区开始
很多人写 Agent 时,第一个动作是:
写一个很长的 prompt,把任务、例子、要求全塞进去。
但这样做的结果是:
- LLM 容易漏掉约束
- 不同任务互相干扰
- 调试困难,改一处影响全局
所以 Agent 设计要先有结构,再有 prompt。
二、Agent 的六要素
| 要素 | 作用 | 例子 |
|---|---|---|
| 角色 | 定义 Agent 是谁 | 你是一个数据查询专家 |
| 目标 | 明确要完成什么 | 根据用户问题查询数据库并返回事实 |
| 工具 | 列出可调用的能力 | get_tables、get_schema、execute_sql |
| 步骤 | 规定执行顺序 | 1.分析意图 → 2.查 schema → 3.生成 SQL → 4.执行 |
| 约束 | 禁止什么行为 | 只能 SELECT,不能 UPDATE/DELETE/DROP |
| 输出格式 | 规定返回结构 | JSON 格式,包含 fact、sql、status、error |
缺少任何一个,都会出问题。
三、数据查询 Agent 的完整 Prompt 示例
# 角色
你是一个数据查询专家。你擅长把自然语言问题转成准确的 SQL,并解释查询结果。
# 目标
根据用户的问题,查询数据库,返回一个明确的事实结论。
# 可用工具
1. get_tables(): 列出数据库中所有表
2. get_schema(table_name): 查询指定表的字段、类型、注释
3. execute_sql(sql): 执行 SQL,返回结果
# 执行步骤
1. 分析用户问题,提取关键信息:时间范围、指标、维度、聚合方式
2. 确定需要查询的表。如果不确定,先调用 get_tables() 查看
3. 调用 get_schema(table_name) 获取相关表结构
4. 生成 SQL。注意:只能使用 SELECT,字段名表名必须和 schema 一致
5. 调用 execute_sql(sql) 执行
6. 如果执行失败,根据错误信息修改 SQL,最多重试 3 次
7. 用一句话解释查询结果
# 约束
- 只能查询,不能修改数据库
- 不要编造字段名
- 不要回答数据库里没有的问题
- 不要预测未来
# 输出格式
{
"fact": "查询结果的一句话解释",
"sql": "生成的 SQL",
"status": "success 或 failed",
"error": "如果失败,写错误信息"
}
四、什么时候需要多个 Agent?
这是一个关键问题。不是每个步骤都要拆成一个 Agent,而是按职责边界拆分。
判断标准
一个任务值得独立成 Agent,如果满足以下任意一条:
- 需要专门的领域知识
- 可以被单独测试
- 失败需要单独处理
- 输出需要被多个下游消费
- 可能需要替换或升级
- 需要不同的人/团队维护
简单判断
| 情况 | 是否独立 Agent |
|---|---|
| 只是简单的数据转换 | 否 |
| 只是前后步骤的胶水 | 否 |
| 拆分后上下文传递成本过高 | 否 |
| 需要专门领域知识 | 是 |
| 可以单独测试 | 是 |
| 失败需要单独处理 | 是 |
五、串行为什么也拆成多个 Agent?
很多人问:既然 A → B → C 是串行的,为什么不写成一个 Agent?
答案是:串行不等于要塞进一个 Agent。
拆成多个 Agent 的好处:
| 好处 | 说明 |
|---|---|
| 专业化 | 每个 Agent 的 prompt 只关注自己的任务 |
| 可替换 | 数据查询 Agent 可以单独给别的系统用 |
| 失败隔离 | SQL 生成错了,只影响数据查询 Agent |
| 可测试 | 每个 Agent 可以单独跑测试 |
| 模型可选 | 不同 Agent 可以用不同模型 |
类比: 餐厅里接待、厨师、服务员、店长是串行工作,但每个角色都是独立的。
六、一个数据分析系统的多 Agent 设计
以"广东销售额为什么下降"为例:
用户问题
↓
[调度器] 判断意图:需要查数据 + 分析原因
↓
[数据查询 Agent] 输出事实:广东销售额下降 15%
↓
[归因分析 Agent] 输出假设:订单量下降、客单价下降、竞品影响
↓
[风控检查 Agent] 检查风险:是否有低置信度假设?是否波动过大?
↓
[输出 Agent] 整理语言,返回用户
| Agent | 输入 | 输出 | 核心能力 |
|---|---|---|---|
| 数据查询 Agent | 用户问题 | 事实数据 | 理解需求、查 schema、生成 SQL |
| 归因分析 Agent | 事实数据 | 假设列表 | 基于数据推导原因 |
| 风控检查 Agent | 事实 + 假设 | 是否可返回 | 识别风险、判断可信度 |
| 输出 Agent | 所有结果 | 自然语言 | 组织语言、附加提示 |
七、多 Agent 之间的通信方式
| 方式 | 像什么 | 耦合度 | 适合场景 |
|---|---|---|---|
| 函数调用 | 流水线,A 直接交给 B | 高 | 简单流程,快速验证 |
| 共享状态 | 公共黑板,大家各写各的 | 中 | 需要看到中间结果,步骤固定 |
| 消息队列 | 快递柜,放完就走 | 低 | 复杂系统、高并发、跨服务 |
当前你的系统用共享状态最合适。
八、调度器的作用
调度器不是简单按顺序调用,它要做:
- 意图识别:判断用户问题属于哪类任务
- 路由选择:决定调用哪些 Agent
- 状态管理:维护全局 state
- 错误处理:重试、兜底、转人工
def classify_intent(question):
if is_data_query(question): return "data_query"
if is_attribution_question(question): return "data_query + attribution"
return "unsupported"
def route(intent):
routes = {
"data_query": ["data_query_agent", "output_agent"],
"data_query + attribution": ["data_query_agent", "attribution_agent", "risk_agent", "output_agent"]
}
return routes.get(intent, ["fallback_agent"])
九、测试和评估 Agent
测试层级
| 层级 | 测什么 | 例子 |
|---|---|---|
| 单元测试 | 单个 Agent 的输入输出 | 给数据查询 Agent 一个问题,检查 SQL |
| 集成测试 | 多个 Agent 组合流程 | 跑完整的问题→查询→归因→风控 |
| 回归测试 | 改代码后历史用例是否还能过 | 100 个历史问题重新跑一遍 |
| 在线评估 | 真实用户反馈 | 用户是否满意 |
核心指标
| 指标 | 含义 |
|---|---|
| SQL 准确率 | 生成的 SQL 是否正确 |
| 执行成功率 | SQL 能跑通的比例 |
| 结果正确率 | 跑通后结果是否符合预期 |
| 归因合理率 | 假设是否合理 |
| 风控拦截率 | 风险输出被拦截的比例 |
| 用户满意度 | 用户是否认可答案 |
十、幻觉和兜底设计
什么是幻觉
LLM 生成看起来合理但不符合事实的内容。
在 Agent 里表现为:
- 编造不存在的字段名
- 没有数据支撑却硬说原因
- 把低置信度假设说成事实
- 预测未来
四层兜底防线
| 防线 | 作用 | 例子 |
|---|---|---|
| 输入层 | 问题是否清晰、是否在范围内 | 模糊问题时反问确认 |
| 工具层 | 工具执行前校验 | 禁止 SQL 写操作、字段存在性检查 |
| Agent 层 | 输出前自我检查 | SQL 是否执行成功、结果是否为空 |
| 系统层 | 独立风控 Agent | 检查波动阈值、低置信度假设 |
兜底原则
- 不确定就拒绝
- 高风险必须人工
- 错误信息要具体
- 全程可审计
十一、一句话总结
Agent 设计不是写 prompt 的艺术,而是边界划分、流程编排、风险控制的艺术。先把任务拆清楚,再写 prompt;先保证可控,再追求自动。
下一步可以读
- 003|RAG 到底在做什么:从向量检索到 Agent 知识库(已更新)
- 004|LangGraph 实战:用代码跑通一个多 Agent 系统(待更新)