第 6 节:技术架构与设计思路

0 阅读9分钟

第 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()

🎯 总结

好的架构设计应该:

  1. 清晰的分层:表现层、业务层、数据层职责明确
  2. 单一职责:每个组件只做一件事
  3. 可测试性:每个部分都可以独立测试
  4. 可扩展性:易于添加新功能
  5. 错误处理:完整的异常处理机制
  6. 性能考虑:合理的优化策略
  7. 安全性:输入验证和错误脱敏

本节小结

本节我们深入学习了 Text-to-BI 系统的技术架构设计:

  1. 三层架构:表现层(Vue 3)、业务层(FastAPI + Agno)、数据层(CubeJS),职责清晰,易于维护
  2. 数据流设计:从用户输入到结果展示的完整流程,包含异步处理、流式响应和错误处理
  3. Workflow 编排:遵循单一职责原则,每个步骤独立可测,易于扩展
  4. Agent 设计:领域专业化、上下文注入、输出格式控制三大要点
  5. 服务集成:封装外部依赖、统一错误处理、支持灵活配置
  6. 前端架构:组件化设计、Composition API 状态管理、SSE 客户端实现
  7. 数据模型:语义化命名、添加描述、合理的粒度设计
  8. 安全性设计:输入验证、SQL 注入防护、错误信息脱敏
  9. 性能优化:查询优化、缓存策略、异步处理

思考与练习

思考题

  1. 为什么要采用三层架构而不是两层或四层?各层的边界如何划分?
  2. 如果要支持多种数据源(MySQL、PostgreSQL、MongoDB),架构需要如何调整?
  3. Workflow 的步骤数量如何确定?过多或过少会有什么问题?
  4. 如何在保证安全性的同时,提供友好的错误提示?

实践练习

  1. 架构图绘制

    • 根据本节内容,绘制完整的系统架构图
    • 标注各组件之间的通信方式
    • 标注数据流转路径
  2. 架构分析

    • 分析本架构的优势和不足
    • 提出至少 3 个改进建议
    • 评估改进的成本和收益
  3. 扩展设计

    • 设计一个支持多租户的架构方案
    • 设计一个支持分布式部署的方案
    • 设计一个支持插件化的扩展机制
  4. 代码实践

    • 实现一个简单的 Workflow 框架
    • 实现一个服务封装类
    • 实现一个错误处理中间件

上一节第 5 节:项目介绍
下一节第 7 节:搭建 FastAPI 基础框架