第 9 节:使用 Agno 构建 AI Agent
阅读时间:约 8 分钟
难度级别:实战
前置知识:FastAPI 基础、CubeJS 集成、AI 基础概念
本节概要
通过本节学习,你将掌握:
- Agno 框架的核心概念和工作原理
- 创建专业的 CubeJS Agent
- 设计有效的 Agent 指令(Instructions)
- 加载和注入数据模型到 Agent
- 测试和优化 Agent 的输出质量
- Agent 的错误处理和调试技巧
引言
Agno 是一个强大的 AI Agent 框架,它让我们能够轻松构建专业的 AI 应用。本节我们将学习如何使用 Agno 创建一个专门理解 CubeJS 查询的 Agent,这是 Text-to-BI 系统的核心组件。
AI Agent 是 Text-to-BI 系统的核心。本文将介绍如何使用 Agno 框架构建一个专业的 CubeJS 查询 Agent。
🎯 本章目标
完成后,你将拥有:
- ✅ 理解 Agno Agent 的工作原理
- ✅ 创建专业的 CubeJS Agent
- ✅ 掌握提示词工程技巧
- ✅ 实现上下文注入
- ✅ 测试 Agent 的输出
🤖 什么是 Agno Agent?
Agent 的概念
Agent 是一个具有特定能力和知识的 AI 实体:
from agno.agent import Agent
from agno.models.deepseek import DeepSeek
agent = Agent(
name="DataAnalyst", # Agent 名称
model=DeepSeek(...), # 使用的模型
instructions="你是数据分析专家...", # 指令
markdown=True # 支持 Markdown 输出
)
# 使用 Agent
response = agent.run("分析这些数据")
Agent vs 普通 LLM 调用
普通 LLM 调用:
# 每次都要重复上下文
response = llm.chat("你是数据分析专家。分析这些数据...")
使用 Agent:
# 上下文已经内置
response = agent.run("分析这些数据")
优势:
- ✅ 上下文复用
- ✅ 专业化能力
- ✅ 一致的行为
- ✅ 易于测试
📝 设计 CubeJS Agent
Step 1: 理解需求
与 AI 对话:
我需要创建一个 CubeJS 查询 Agent,要求:
1. 理解 CubeJS 数据模型(YAML 格式)
2. 将自然语言转换为 CubeJS REST API 查询 JSON
3. 输出格式规范,包含查询分析和 JSON 代码块
4. 支持常见的查询类型:统计、分组、过滤、排序
请帮我设计这个 Agent 的指令。
Step 2: 加载数据模型
创建模型加载函数:
"""
CubeJS Agent - 专业的 CubeJS 查询生成器
"""
import os
import yaml
from agno.agent import Agent
from agno.models.deepseek import DeepSeek
def load_cubejs_schema(schema_path: str) -> dict:
"""
加载单个 CubeJS YAML 模型文件
Args:
schema_path: YAML 文件路径
Returns:
解析后的模型字典
"""
with open(schema_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def load_all_cubejs_schemas(model_dir: str) -> dict:
"""
加载目录下所有的 CubeJS 模型文件
Args:
model_dir: 模型目录路径
Returns:
所有模型的字典,键为模型名称
"""
schemas = {}
try:
for filename in os.listdir(model_dir):
if filename.endswith('.cube.yml'):
schema_path = os.path.join(model_dir, filename)
cube_name = filename.replace('.cube.yml', '')
schemas[cube_name] = load_cubejs_schema(schema_path)
except FileNotFoundError:
schemas = {"error": "Model directory not found"}
return schemas
Step 3: 构建 Agent 指令
关键要素:
- 角色定义:明确 Agent 的身份
- 数据模型:注入可用的数据结构
- 输出格式:规范输出格式
- 示例:提供参考示例
- 规则:明确约束条件
完整的 Agent 构建函数:
def build_cubejs_agent():
"""
构建 CubeJS 专家 Agent
Returns:
配置好的 Agno Agent
"""
# 加载所有数据模型
model_dir = os.path.join(
os.path.dirname(__file__),
'../cubejs/model'
)
try:
all_schemas = load_all_cubejs_schemas(model_dir)
except Exception as e:
all_schemas = {"error": f"Failed to load schemas: {str(e)}"}
# 将模型转换为 YAML 字符串
schema_yaml = yaml.dump(all_schemas, default_flow_style=False, allow_unicode=True)
# 构建详细的指令
instructions = f"""你是 CubeJS 查询生成器。将自然语言转换为 CubeJS JSON 查询。
**可用的数据模型:**
{schema_yaml}
**响应格式(严格遵守):**
## 🔍 查询分析
[一句话说明查询目的]
```json
[JSON query here - properly formatted]
示例:
用户:"统计员工总数" 响应:
🔍 查询分析
统计数据库中的员工总数
{{
"measures": ["employees.total_employees"]
}}
用户:"按性别统计员工数量" 响应:
🔍 查询分析
按性别分组统计员工数量
{{
"measures": ["employees.total_employees"],
"dimensions": ["employees.gender"]
}}
用户:"各部门的员工数量" 响应:
🔍 查询分析
按部门分组统计员工数量
{{
"measures": ["employees.total_employees"],
"dimensions": ["employees.dept_name"]
}}
规则:
- 查询分析只用一句话
- JSON 代码块前后必须有空行
- 确保 JSON 格式正确,有正确的缩进
- 使用正确的 cube 前缀(如 "employees.measure_name")
- 只包含必要的查询属性
- JSON 代码块后不要添加额外的说明
现在生成查询:"""
# 创建 Agent
agent = Agent(
model=DeepSeek(id="deepseek-chat"),
instructions=instructions,
markdown=True,
debug_mode=False,
)
return agent
## 🎨 提示词工程技巧
### 1. 结构化指令
使用清晰的结构组织指令:
角色定义 你是...
可用资源 数据模型:...
任务描述 将...转换为...
输出格式 格式:...
示例 示例1:... 示例2:...
规则
- 规则1
- 规则2
### 2. 提供具体示例
```python
instructions = """
**示例:**
输入:"统计员工总数"
输出:
```json
{
"measures": ["employees.total_employees"]
}
输入:"按性别统计" 输出:
{
"measures": ["employees.total_employees"],
"dimensions": ["employees.gender"]
}
"""
### 3. 明确约束条件
```python
instructions = """
**规则:**
- 必须使用 JSON 格式
- 必须包含 measures 或 dimensions
- cube 名称必须加前缀
- 不要添加注释
- 不要使用未定义的字段
"""
4. 格式控制
instructions = """
**输出格式(严格遵守):**
## 🔍 查询分析
[一句话]
```json
{JSON内容}
注意:
- JSON 前后必须有空行
- 不要添加额外内容 """
## 🧪 测试 Agent
### Step 1: 创建测试函数
```python
def query_cubejs(user_question: str, stream: bool = False):
"""
使用 CubeJS Agent 处理查询
Args:
user_question: 用户的自然语言问题
stream: 是否使用流式输出
Returns:
Agent 的响应
"""
agent = build_cubejs_agent()
if stream:
return agent.run(user_question, stream=True)
else:
response = agent.run(user_question, stream=False)
return response.content
Step 2: 测试不同场景
创建测试脚本:
if __name__ == "__main__":
from dotenv import load_dotenv
load_dotenv()
# 测试用例
test_cases = [
"统计员工总数",
"按性别统计员工数量",
"显示各部门的员工分布",
"查询女性员工数量",
]
print("=== CubeJS Agent 测试 ===\n")
for question in test_cases:
print(f"问题: {question}")
print("-" * 60)
response = query_cubejs(question)
print(response)
print("\n" + "=" * 60 + "\n")
运行测试:
cd backend
python agents/cubejs_agent.py
预期输出:
=== CubeJS Agent 测试 ===
问题: 统计员工总数
------------------------------------------------------------
## 🔍 查询分析
统计数据库中的员工总数
```json
{
"measures": ["employees.total_employees"]
}
============================================================
问题: 按性别统计员工数量
🔍 查询分析
按性别分组统计员工数量
{
"measures": ["employees.total_employees"],
"dimensions": ["employees.gender"]
}
============================================================
## 🔍 调试 Agent
### 启用调试模式
```python
agent = Agent(
model=DeepSeek(id="deepseek-chat"),
instructions=instructions,
markdown=True,
debug_mode=True, # 启用调试
)
调试输出:
[DEBUG] Agent: CubeJSExpert
[DEBUG] Input: 统计员工总数
[DEBUG] Model: deepseek-chat
[DEBUG] Instructions length: 1234 chars
[DEBUG] Response time: 1.23s
[DEBUG] Output length: 156 chars
验证输出格式
import json
import re
def validate_agent_output(output: str) -> bool:
"""
验证 Agent 输出格式
Args:
output: Agent 的输出
Returns:
是否符合格式要求
"""
# 检查是否包含查询分析
if "## 🔍 查询分析" not in output:
print("❌ 缺少查询分析")
return False
# 提取 JSON 代码块
json_pattern = r'```json\s*([\s\S]*?)\s*```'
matches = re.findall(json_pattern, output)
if not matches:
print("❌ 缺少 JSON 代码块")
return False
# 验证 JSON 格式
try:
query = json.loads(matches[0])
print("✅ JSON 格式正确")
print(f" Measures: {query.get('measures', [])}")
print(f" Dimensions: {query.get('dimensions', [])}")
return True
except json.JSONDecodeError as e:
print(f"❌ JSON 解析失败: {e}")
return False
💡 优化 Agent 性能
1. 缓存数据模型
from functools import lru_cache
@lru_cache(maxsize=1)
def load_all_cubejs_schemas_cached(model_dir: str) -> dict:
"""缓存数据模型加载"""
return load_all_cubejs_schemas(model_dir)
2. 减少指令长度
# ❌ 冗长的指令
instructions = """
你是一个非常专业的 CubeJS 查询生成器...
你需要理解用户的自然语言问题...
然后将其转换为 CubeJS 的 REST API 查询格式...
...(大量重复说明)
"""
# ✅ 简洁的指令
instructions = """
你是 CubeJS 查询生成器。将自然语言转换为 CubeJS JSON 查询。
数据模型:{schema}
输出格式:
## 🔍 查询分析
[说明]
```json
{查询}
"""
### 3. 使用更快的模型
```python
# 对于简单查询,使用更快的模型
agent = Agent(
model=DeepSeek(id="deepseek-chat"), # 快速模型
# model=DeepSeek(id="deepseek-coder"), # 代码专用
)
🎯 Vibe Coding 要点
1. 迭代优化提示词
第1版:"生成 CubeJS 查询"
测试 → 输出不规范
第2版:"生成 JSON 格式的 CubeJS 查询"
测试 → 缺少说明
第3版:"生成查询分析 + JSON 查询"
测试 → 格式不一致
第4版:"严格按照格式输出"
测试 → ✅ 符合要求
2. 使用示例引导
提供 3-5 个典型示例,覆盖主要场景:
- 简单统计
- 分组查询
- 过滤条件
- 排序
3. 及时验证
每次修改指令后,立即测试:
python agents/cubejs_agent.py
4. 保存成功的提示词
将有效的提示词模板保存下来,用于其他 Agent。
本节小结
本节我们完成了 AI Agent 的构建:
- Agno 框架:理解了 Agent、Model、Instructions 等核心概念
- Agent 创建:使用 DeepSeek 模型创建了专业的 CubeJS Agent
- 指令设计:编写了清晰、具体的 Agent 指令,包含角色、任务、输出格式
- 模型注入:将 CubeJS 数据模型动态注入到 Agent 指令中
- 测试优化:通过多轮测试优化 Agent 的输出质量
- 最佳实践:掌握了提示词工程的关键技巧
现在我们有了一个能够理解自然语言并生成 CubeJS 查询的 Agent。
思考与练习
思考题
- 为什么要创建专业的 Agent 而不是使用通用的 ChatGPT?
- Agent 指令中哪些部分最重要?如何平衡详细程度和简洁性?
- 如何评估 Agent 的输出质量?有哪些量化指标?
- 如果 Agent 经常生成错误的查询,应该如何调试和优化?
实践练习
-
创建新 Agent:
- 创建一个结果分析 Agent
- 输入查询结果,输出数据分析和建议
- 测试不同类型的数据
-
优化指令:
- 尝试不同的指令写法
- 对比输出质量
- 找出最佳的指令模板
-
错误处理:
- 故意输入模糊的查询
- 观察 Agent 如何处理
- 改进指令以提高容错性
-
多轮对话:
- 实现 Agent 的对话历史记录
- 支持上下文相关的查询
- 测试多轮对话效果