第 6 节:技术架构与设计思路
阅读时间:约 8 分钟
难度级别:进阶
前置知识:了解项目背景和功能特性
本节概要
通过本节学习,你将掌握:
- Text-to-BI 系统的三层架构设计及其优势
- 完整的数据流转过程和关键设计点
- Workflow 编排的设计原则和可扩展性
- Agent 专业化设计的最佳实践
- 服务集成的封装方法和错误处理
- 前端架构和状态管理策略
引言
好的架构是项目成功的基础。本节将详细介绍 Text-to-BI 系统的技术架构设计,以及在使用 Vibe Coding 开发时的架构决策思路。通过学习这些设计思想,你将能够设计出可扩展、易维护的 AI 应用架构。
🏗️ 整体架构
三层架构
┌─────────────────────────────────────┐
│ 表现层 (Presentation) │
│ Vue 3 + TypeScript + Naive UI │
└──────────────┬──────────────────────┘
│ HTTP/SSE
┌──────────────▼──────────────────────┐
│ 业务层 (Business) │
│ FastAPI + Agno + Workflow │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ 数据层 (Data) │
│ CubeJS + SQLite │
└─────────────────────────────────────┘
为什么选择这个架构?
表现层:Vue 3
- ✅ 响应式系统强大
- ✅ TypeScript 支持好
- ✅ 生态系统完善
- ✅ 学习曲线平缓
业务层:FastAPI + Agno
- ✅ 异步性能优秀
- ✅ 类型提示完整
- ✅ 自动生成文档
- ✅ Workflow 编排能力
数据层:CubeJS
- ✅ 统一数据模型
- ✅ 查询优化
- ✅ REST API 标准
- ✅ 易于扩展
🔄 数据流设计
完整的数据流
用户输入
↓
┌──────────────────┐
│ 前端 Vue 组件 │
│ - 收集用户输入 │
│ - 建立 SSE 连接 │
└────────┬─────────┘
│ POST /workflow/query
│
┌────────▼─────────┐
│ FastAPI 路由 │
│ - 参数验证 │
│ - 调用 Workflow │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Agno Workflow │
│ - 步骤编排 │
│ - 流式输出 │
└────────┬─────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌──▼────┐
│ Agent │ │Function│
│ 步骤 │ │ 步骤 │
└───┬───┘ └──┬────┘
│ │
│ ┌───▼────┐
│ │CubeJS │
│ │Service │
│ └───┬────┘
│ │
│ ┌───▼────┐
│ │CubeJS │
│ │ API │
│ └───┬────┘
│ │
│ ┌───▼────┐
│ │SQLite │
│ │Database│
│ └────────┘
│
┌───▼────────────┐
│ 流式响应 │
│ - SSE 格式 │
│ - 实时推送 │
└───┬────────────┘
│
┌───▼────────────┐
│ 前端渲染 │
│ - Markdown │
│ - 表格展示 │
└────────────────┘
关键设计点
1. 异步处理
# FastAPI 异步路由
@router.post("/query")
async def query_workflow(request: WorkflowRequest):
# 异步执行 Workflow
async for event in workflow.run(request.message, stream=True):
yield format_sse(event)
优势:
- 不阻塞其他请求
- 提高并发能力
- 更好的资源利用
2. 流式响应
# SSE 格式
data: {"content": "查询分析..."}\n\n
data: {"content": "```json\n"}\n\n
data: {"content": "{\n"}\n\n
data: {"content": " \"measures\": [...]\n"}\n\n
data: {"content": "}\n"}\n\n
data: {"content": "```\n"}\n\n
data: [DONE]\n\n
优势:
- 实时反馈
- 用户体验好
- 减少等待焦虑
3. 错误处理
try:
result = await workflow.run(query)
except ValidationError as e:
return {"error": "输入验证失败", "details": str(e)}
except ServiceError as e:
return {"error": "服务调用失败", "details": str(e)}
except Exception as e:
logger.error(f"未预期的错误: {e}")
return {"error": "系统错误"}
🤖 Workflow 设计
Workflow 步骤分解
text_to_bi_workflow = Workflow(
name="TextToBIWorkflow",
steps=[
# 步骤1: 理解用户意图,生成 CubeJS 查询
cubejs_agent,
# 步骤2: 获取 SQL 并执行查询
get_sql_and_execute,
# 步骤3: 格式化查询结果
format_results,
# 步骤4: 分析数据并提供洞察
result_formatter,
]
)
为什么这样设计?
单一职责原则
每个步骤只做一件事:
# ✅ 好的设计
def get_sql_and_execute(step_input):
"""只负责获取 SQL 和执行查询"""
query = extract_query(step_input)
sql = service.get_sql(query)
results = service.execute(query)
return StepOutput(content=format_data(sql, results))
# ❌ 不好的设计
def do_everything(step_input):
"""做所有事情"""
# 解析输入
# 生成查询
# 获取 SQL
# 执行查询
# 格式化结果
# 分析数据
# ...
可测试性
每个步骤可以独立测试:
def test_get_sql_and_execute():
# 准备测试数据
step_input = StepInput(content='{"measures": ["employees.total"]}')
# 执行步骤
result = get_sql_and_execute(step_input)
# 验证结果
assert "SELECT" in result.content
assert "employees" in result.content
可扩展性
轻松添加新步骤:
# 添加数据验证步骤
def validate_results(step_input):
"""验证查询结果的合理性"""
results = extract_results(step_input)
if not results:
return StepOutput(content="警告:查询无结果")
return StepOutput(content="")
# 插入到 Workflow
workflow = Workflow(
steps=[
cubejs_agent,
get_sql_and_execute,
validate_results, # 新增步骤
format_results,
result_formatter,
]
)
🎯 Agent 设计
CubeJS Agent 的设计
def build_cubejs_agent():
# 加载数据模型
schemas = load_all_cubejs_schemas("cubejs/model")
# 构建指令
instructions = f"""
你是 CubeJS 查询专家。
可用的数据模型:
{yaml.dump(schemas)}
任务:将自然语言转换为 CubeJS REST API 查询 JSON。
输出格式:
## 🔍 查询分析
[一句话说明]
```json
{{
"measures": [...],
"dimensions": [...]
}}
```
"""
return Agent(
model=DeepSeek(id="deepseek-chat"),
instructions=instructions,
markdown=True
)
设计要点
1. 领域专业化
Agent 专注于特定领域(CubeJS 查询),而不是通用对话:
# ✅ 专业 Agent
cubejs_agent = Agent(
name="CubeJSExpert",
instructions="你是 CubeJS 查询专家,只生成查询 JSON"
)
# ❌ 通用 Agent
general_agent = Agent(
name="GeneralAssistant",
instructions="你是通用助手,可以做任何事"
)
2. 上下文注入
将数据模型注入到 Agent 的指令中:
# 动态加载数据模型
schemas = load_all_cubejs_schemas()
# 注入到指令
instructions = f"""
可用的数据模型:
{yaml.dump(schemas)}
"""
优势:
- Agent 理解数据结构
- 生成准确的查询
- 减少错误率
3. 输出格式控制
明确指定输出格式:
instructions = """
输出格式(严格遵守):
## 🔍 查询分析
[一句话说明查询目的]
```json
{
"measures": ["cube.measure"],
"dimensions": ["cube.dimension"]
}
规则:
- 必须有空行
- JSON 必须有效
- 不要添加额外说明 """
## 🔌 服务集成设计
### CubeJS Service
```python
class CubeJSService:
def __init__(self, base_url: str):
self.base_url = base_url
self.session = requests.Session()
def sql(self, query: Dict) -> Dict:
"""获取 SQL 语句"""
response = self.session.post(
f"{self.base_url}/cubejs-api/v1/sql",
json={"query": query}
)
response.raise_for_status()
return response.json()
def load(self, query: Dict) -> Dict:
"""执行查询获取数据"""
response = self.session.post(
f"{self.base_url}/cubejs-api/v1/load",
json={"query": query}
)
response.raise_for_status()
return response.json()
设计原则
1. 封装外部依赖
# ✅ 好的设计:封装 CubeJS API
service = CubeJSService(base_url)
results = service.load(query)
# ❌ 不好的设计:直接调用
response = requests.post("http://localhost:4000/cubejs-api/v1/load", ...)
2. 统一错误处理
class ServiceError(Exception):
"""服务调用错误"""
pass
class CubeJSService:
def load(self, query: Dict) -> Dict:
try:
response = self.session.post(url, json=query)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
raise ServiceError(f"CubeJS 查询失败: {e}")
3. 可配置性
class CubeJSService:
def __init__(
self,
base_url: str,
api_token: Optional[str] = None,
timeout: int = 30
):
self.base_url = base_url
self.timeout = timeout
self.session = requests.Session()
if api_token:
self.session.headers["Authorization"] = f"Bearer {api_token}"
🎨 前端架构
组件设计
App.vue
├── Layout.vue
├── ChatPage.vue
│ ├── MessageList
│ ├── MessageInput
│ └── MarkdownRenderer
│
└── WorkflowPage.vue
├── QueryInput
├── WorkflowSteps
└── ResultDisplay
状态管理
使用 Vue 3 Composition API:
// useWorkflow.ts
export function useWorkflow() {
const isLoading = ref(false)
const allContent = ref('')
const executeWorkflow = async (query: string) => {
isLoading.value = true
allContent.value = ''
await streamWorkflow(
{ message: query },
(content: string) => {
allContent.value += content
}
)
isLoading.value = false
}
return {
isLoading,
allContent,
executeWorkflow
}
}
SSE 客户端实现
export const streamWorkflow = (
params: { message: string },
onMessage: (content: string) => void
) => {
return post({
url: '/workflow/query',
data: params,
responseType: 'text',
onDownloadProgress: (event) => {
const rawData = event.target.response
const lines = rawData.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
if (data === '[DONE]') return
const parsed = JSON.parse(data)
onMessage(parsed)
}
}
}
})
}
📊 数据模型设计
CubeJS 数据模型
# employees.cube.yml
cubes:
- name: employees
sql: SELECT * FROM employees
measures:
- name: total_employees
type: count
description: 员工总数
- name: avg_salary
type: avg
sql: salary
description: 平均工资
dimensions:
- name: gender
sql: gender
type: string
description: 性别
- name: dept_name
sql: dept_name
type: string
description: 部门名称
- name: hire_date
sql: hire_date
type: time
description: 入职日期
设计原则
1. 语义化命名
# ✅ 好的命名
measures:
- name: total_employees
- name: avg_salary
# ❌ 不好的命名
measures:
- name: cnt
- name: avg_sal
2. 添加描述
measures:
- name: total_employees
type: count
description: 员工总数 # 帮助 AI 理解
3. 合理的粒度
# 基础维度
dimensions:
- name: gender
- name: dept_name
# 时间维度
- name: hire_date
type: time
# 计算维度
- name: tenure_years
sql: DATEDIFF(YEAR, hire_date, CURRENT_DATE)
🔒 安全性设计
1. 输入验证
from pydantic import BaseModel, validator
class WorkflowRequest(BaseModel):
message: str
@validator('message')
def validate_message(cls, v):
if not v or not v.strip():
raise ValueError('消息不能为空')
if len(v) > 1000:
raise ValueError('消息过长')
return v.strip()
2. SQL 注入防护
使用 CubeJS 的参数化查询:
# ✅ 安全:使用 CubeJS API
query = {
"measures": ["employees.total"],
"filters": [{
"member": "employees.dept_name",
"operator": "equals",
"values": [dept_name] # 参数化
}]
}
# ❌ 不安全:直接拼接 SQL
sql = f"SELECT * FROM employees WHERE dept = '{dept_name}'"
3. 错误信息脱敏
try:
result = service.query(sql)
except Exception as e:
# ❌ 不要暴露内部错误
# return {"error": str(e)}
# ✅ 返回友好的错误信息
logger.error(f"查询失败: {e}")
return {"error": "查询执行失败,请稍后重试"}
🚀 性能优化
1. 查询优化
# 限制返回行数
def format_results(results: Dict) -> str:
data = results.get("data", [])
display_count = min(len(data), 20) # 最多显示 20 行
# 只处理需要显示的数据
for row in data[:display_count]:
# 格式化...
2. 缓存策略
from functools import lru_cache
@lru_cache(maxsize=100)
def load_cubejs_schemas(model_dir: str) -> Dict:
"""缓存数据模型加载"""
return load_all_schemas(model_dir)
3. 异步处理
# 使用异步 HTTP 客户端
import httpx
class CubeJSService:
def __init__(self):
self.client = httpx.AsyncClient()
async def load(self, query: Dict) -> Dict:
response = await self.client.post(url, json=query)
return response.json()
🎯 总结
好的架构设计应该:
- 清晰的分层:表现层、业务层、数据层职责明确
- 单一职责:每个组件只做一件事
- 可测试性:每个部分都可以独立测试
- 可扩展性:易于添加新功能
- 错误处理:完整的异常处理机制
- 性能考虑:合理的优化策略
- 安全性:输入验证和错误脱敏
本节小结
本节我们深入学习了 Text-to-BI 系统的技术架构设计:
- 三层架构:表现层(Vue 3)、业务层(FastAPI + Agno)、数据层(CubeJS),职责清晰,易于维护
- 数据流设计:从用户输入到结果展示的完整流程,包含异步处理、流式响应和错误处理
- Workflow 编排:遵循单一职责原则,每个步骤独立可测,易于扩展
- Agent 设计:领域专业化、上下文注入、输出格式控制三大要点
- 服务集成:封装外部依赖、统一错误处理、支持灵活配置
- 前端架构:组件化设计、Composition API 状态管理、SSE 客户端实现
- 数据模型:语义化命名、添加描述、合理的粒度设计
- 安全性设计:输入验证、SQL 注入防护、错误信息脱敏
- 性能优化:查询优化、缓存策略、异步处理
思考与练习
思考题
- 为什么要采用三层架构而不是两层或四层?各层的边界如何划分?
- 如果要支持多种数据源(MySQL、PostgreSQL、MongoDB),架构需要如何调整?
- Workflow 的步骤数量如何确定?过多或过少会有什么问题?
- 如何在保证安全性的同时,提供友好的错误提示?
实践练习
-
架构图绘制:
- 根据本节内容,绘制完整的系统架构图
- 标注各组件之间的通信方式
- 标注数据流转路径
-
架构分析:
- 分析本架构的优势和不足
- 提出至少 3 个改进建议
- 评估改进的成本和收益
-
扩展设计:
- 设计一个支持多租户的架构方案
- 设计一个支持分布式部署的方案
- 设计一个支持插件化的扩展机制
-
代码实践:
- 实现一个简单的 Workflow 框架
- 实现一个服务封装类
- 实现一个错误处理中间件
上一节:第 5 节:项目介绍
下一节:第 7 节:搭建 FastAPI 基础框架