01 一次完整的AI功能上线,到底要跨过多少道坎?

假设产品经理提了一个看似简单的需求:“在客服系统里加一个智能问答功能,用户问问题,AI根据公司知识库回答。”

这个需求如果交给一个只懂Python的算法工程师,他可能会:

  1. 在Jupyter Notebook里写一段调用OpenAI API的代码
  2. 测试几条用例,效果不错
  3. 用Flask起一个服务,调通接口
  4. 告诉前端"你直接调这个接口就行"
  5. 然后——就没有然后了

这个功能最终上线了吗?没有。原因很简单:在"能跑"和"能上线"之间,至少隔着10道工序。

让我们把这10道工序一一拆解。

02 第一道关:接口设计——不只是"能调通"

接口设计是全栈AI工程师的第一道考题。很多人认为接口设计就是"定义几个URL、几个参数",但真正的接口设计要考虑的东西远不止这些。

好的AI接口长什么样?

# ❌ 反面教材:一个"能调通"但"不可用"的接口
@app.post("/chat")
def chat(question: str):
    # 没有限流、没有鉴权、没有超时、没有日志、没有错误处理
    result = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": question}]
    )
    return {"answer": result.choices[0].message.content}

这个接口能调通吗?能。能上线吗?绝对不行。

# ✅ 正面教材:生产级AI接口设计
from fastapi import FastAPI, HTTPException, Depends, Header, Request
from pydantic import BaseModel, Field, validator
from typing import Optional, Literal
import time
import uuid
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address

app = FastAPI()
limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"])
app.state.limiter = limiter

# 【设计要点1】严格定义的请求/响应模型
class ChatRequest(BaseModel):
    """请求模型:强类型校验,防止脏数据"""
    question: str = Field(..., min_length=1, max_length=2000, description="用户问题")
    session_id: Optional[str] = Field(None, description="会话ID,用于上下文延续")
    stream: bool = Field(False, description="是否流式返回")
    temperature: float = Field(0.7, ge=0.0, le=1.0, description="生成温度")
    
    @validator('question')
    def sanitize_question(cls, v):
        """输入清洗:防止注入"""
        import re
        # 移除潜在的注入尝试
        suspicious = re.search(r'ignore.*instruction|system prompt|之前.*内容', v, re.I)
        if suspicious:
            raise ValueError("包含不安全的内容")
        return v

class ChatResponse(BaseModel):
    """响应模型:统一输出格式,包含元数据"""
    answer: str
    session_id: str
    tokens_used: int
    latency_ms: float
    confidence: Optional[float] = None
    sources: Optional[list[str]] = None
    trace_id: str
    is_fallback: bool = False


# 【设计要点2】完整的鉴权与限流
@app.post("/api/v1/chat")
@limiter.limit("50/minute")
async def chat_endpoint(
    request: Request,
    req: ChatRequest,
    x_user_id: str = Header(..., description="用户ID"),
    x_tenant_id: str = Header(..., description="租户ID")
):
    """
    完整的AI问答接口
    
    特性:
    - 请求/响应强类型
    - 用户级限流(50次/分钟)
    - 租户级数据隔离
    - 请求追踪(trace_id)
    - 超时控制(通过中间件实现)
    """
    trace_id = f"trace_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
    start_time = time.time()
    
    try:
        # 1. 验证用户权限(伪代码)
        # await auth_service.verify_permission(x_user_id, x_tenant_id, "chat")
        
        # 2. 获取会话上下文
        # context = await session_service.get_context(req.session_id) if req.session_id else None
        
        # 3. 构建Prompt
        # prompt = build_prompt(req.question, context)
        
        # 4. 调用LLM(带超时)
        # result = await llm_service.invoke_with_timeout(prompt, timeout=30)
        
        # 这里用模拟数据演示
        await asyncio.sleep(0.5)
        result = {
            "answer": f"这是对「{req.question}」的模拟回答",
            "tokens_used": 150,
            "sources": ["知识库文档v2.3.pdf", "FAQ_2024.md"]
        }
        
        # 5. 记录对话历史
        # await session_service.save_message(x_user_id, req.question, result["answer"])
        
        # 6. 返回结构化响应
        return ChatResponse(
            answer=result["answer"],
            session_id=req.session_id or uuid.uuid4().hex,
            tokens_used=result["tokens_used"],
            latency_ms=(time.time() - start_time) * 1000,
            confidence=0.92,
            sources=result["sources"],
            trace_id=trace_id,
            is_fallback=False
        )
        
    except TimeoutError:
        # 【设计要点4】降级处理
        return ChatResponse(
            answer="AI服务响应超时,已记录您的请求,稍后将通过站内信回复您。",
            session_id=req.session_id or uuid.uuid4().hex,
            tokens_used=0,
            latency_ms=(time.time() - start_time) * 1000,
            trace_id=trace_id,
            is_fallback=True
        )
    except Exception as e:
        # 【设计要点5】结构化错误响应
        raise HTTPException(
            status_code=500,
            detail={
                "code": "AI_SERVICE_ERROR",
                "message": str(e),
                "trace_id": trace_id,
                "suggestion": "请稍后重试,如问题持续请联系技术支持"
            }
        )

接口设计的核心原则:

  • 强类型:用Pydantic定义请求/响应模型,自文档化
  • 可观测:每个请求都带trace_id,所有日志可追溯
  • 防攻击:输入清洗、注入检测、限流
  • 可降级:超时/异常时有合理的fallback响应

03 第二道关:建模——不只是"调参"

这里的"建模"不是指训练大模型,而是指为AI应用建立正确的数据模型和业务模型。

3.1 数据建模:向量库的Schema设计

# 向量库文档Schema设计
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime

class KnowledgeDocument(BaseModel):
    """知识库文档的完整数据模型"""
    
    # 标识字段
    doc_id: str  # 唯一标识
    version: int = 1
    
    # 内容字段
    title: str
    content: str
    content_hash: str  # 用于去重和增量更新
    
    # 切片信息(RAG)
    chunks: List[dict]  # [{"index": 0, "text": "...", "embedding": [...]}]
    chunk_strategy: str  # "fixed_size" | "semantic" | "recursive"
    
    # 元数据
    source_type: str  # "wiki" | "confluence" | "local_file" | "external_api"
    source_url: Optional[str] = None
    author: Optional[str] = None
    department: str  # 所属部门,用于权限控制
    tags: List[str] = []
    
    # 权限字段(关键!)
    allowed_roles: List[str]  # ["admin", "member", "guest"]
    allowed_users: List[str] = []  # 特定用户白名单
    is_public: bool = False
    
    # 时间戳
    created_at: datetime
    updated_at: datetime
    expires_at: Optional[datetime] = None  # 知识有效期
    
    # 统计字段
    access_count: int = 0
    last_accessed_at: Optional[datetime] = None
    relevance_score: float = 0.0  # 由用户反馈动态调整

3.2 业务建模:问答流程的状态机

AI问答不是简单的"请求-响应",而是一个多步骤的状态流转过程。

from enum import Enum
from typing import Optional, Dict, Any
from dataclasses import dataclass, field

class ConversationState(Enum):
    """会话状态枚举"""
    INIT = "init"                      # 初始状态
    CLARIFYING = "clarifying"          # 澄清问题(意图不明确)
    RETRIEVING = "retrieving"          # 检索中
    GENERATING = "generating"          # 生成中
    CONFIRMING = "confirming"          # 确认理解(高风险操作)
    WAITING_HUMAN = "waiting_human"    # 等待人工接管
    COMPLETED = "completed"            # 已完成
    ABANDONED = "abandoned"            # 已放弃

@dataclass
class ConversationFlow:
    """
    问答流程的状态机
    管理从用户提问到答案输出的完整生命周期
    """
    session_id: str
    user_id: str
    state: ConversationState = ConversationState.INIT
    context: Dict[str, Any] = field(default_factory=dict)
    attempts: int = 0
    max_attempts: int = 3
    
    def transition(self, new_state: ConversationState, data: Optional[Dict] = None):
        """
        状态转换,带合法性校验
        """
        # 定义合法的状态转换路径
        valid_transitions = {
            ConversationState.INIT: [
                ConversationState.CLARIFYING,
                ConversationState.RETRIEVING,
                ConversationState.WAITING_HUMAN
            ],
            ConversationState.CLARIFYING: [
                ConversationState.RETRIEVING,
                ConversationState.WAITING_HUMAN
            ],
            ConversationState.RETRIEVING: [
                ConversationState.GENERATING,
                ConversationState.WAITING_HUMAN
            ],
            ConversationState.GENERATING: [
                ConversationState.CONFIRMING,
                ConversationState.COMPLETED,
                ConversationState.WAITING_HUMAN
            ],
            ConversationState.CONFIRMING: [
                ConversationState.COMPLETED,
                ConversationState.ABANDONED
            ],
            ConversationState.WAITING_HUMAN: [
                ConversationState.COMPLETED,
                ConversationState.ABANDONED
            ],
            ConversationState.COMPLETED: [],
            ConversationState.ABANDONED: []
        }
        
        if new_state not in valid_transitions.get(self.state, []):
            raise ValueError(
                f"非法状态转换: {self.state.value} -> {new_state.value}"
            )
        
        self.state = new_state
        if data:
            self.context.update(data)
        
        # 状态转换日志
        print(f"[会话{self.session_id}] {self.state.value} -> {new_state.value}")
    
    def should_escalate_to_human(self) -> bool:
        """判断是否需要转人工"""
        conditions = [
            self.attempts >= self.max_attempts,
            self.context.get("sentiment_score", 0) < -0.5,  # 负面情绪强烈
            self.context.get("intent") == "complaint",      # 投诉意图
            self.state == ConversationState.WAITING_HUMAN
        ]
        return any(conditions)

04 第三道关:容器化部署——让AI服务"到处可跑"

模型是Python写的,依赖是一堆C++库,GPU驱动版本要求苛刻。每次部署都是一场"环境灾难"。

容器化解决了这个问题——但前提是你会正确地容器化。

4.1 生产级Dockerfile

# 多阶段构建:减小最终镜像体积
FROM python:3.11-slim AS builder

# 安装编译依赖
RUN apt-get update && apt-get install -y \
    build-essential \
    curl \
    && rm -rf /var/lib/apt/lists/*

# 安装Python依赖(利用Docker缓存层)
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# -------- 生产阶段 ----------
FROM python:3.11-slim

# 安装运行时依赖
RUN apt-get update && apt-get install -y \
    curl \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# 从构建阶段复制已安装的依赖
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

WORKDIR /app

# 复制应用代码
COPY . .

# 创建非root用户(安全最佳实践)
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

4.2 Kubernetes部署配置

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-chat-service
  namespace: ai-platform
  labels:
    app: ai-chat-service
spec:
  replicas: 3  # 水平扩展,保障高可用
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0  # 滚动更新期间零停机
  selector:
    matchLabels:
      app: ai-chat-service
  template:
    metadata:
      labels:
        app: ai-chat-service
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8000"
        prometheus.io/path: "/metrics"
    spec:
      containers:
      - name: ai-chat
        image: ai-registry.internal/ai-chat-service:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8000
          name: http
        env:
        - name: OPENAI_API_KEY
          valueFrom:
            secretKeyRef:
              name: openai-secret
              key: api-key
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: redis-secret
              key: url
        - name: LOG_LEVEL
          value: "INFO"
        - name: MAX_WORKERS
          value: "4"
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
            cpu: "2000m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 20
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 3
          failureThreshold: 3
        volumeMounts:
        - name: model-cache
          mountPath: /app/.cache
      volumes:
      - name: model-cache
        persistentVolumeClaim:
          claimName: model-cache-pvc
---
# 自动扩缩容配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ai-chat-hpa
  namespace: ai-platform
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ai-chat-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  - type: Pods
    pods:
      metric:
        name: ai_requests_queue_depth
      target:
        type: AverageValue
        averageValue: "10"

05 第四道关:监控——让系统的"脉搏"清晰可见

没有监控的系统就像没有仪表的飞机——你不知道什么时候会掉下来。

5.1 业务指标监控

from prometheus_client import Counter, Histogram, Gauge, generate_latest, REGISTRY
from prometheus_client import CONTENT_TYPE_LATEST
import time
from functools import wraps

# 定义监控指标
class AIMetrics:
    """AI服务监控指标体系"""
    
    # Counter: 累加型指标
    request_total = Counter(
        'ai_request_total',
        'AI请求总数',
        ['endpoint', 'status', 'model']
    )
    
    # Histogram: 分布型指标(延迟)
    request_duration = Histogram(
        'ai_request_duration_seconds',
        'AI请求耗时',
        ['endpoint', 'model'],
        buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0]
    )
    
    # Histogram: Token消耗
    token_usage = Histogram(
        'ai_token_usage_total',
        'Token消耗量',
        ['model', 'type'],
        buckets=[10, 50, 100, 250, 500, 1000, 2000, 5000, 10000]
    )
    
    # Gauge: 瞬时值(当前并发数)
    concurrent_requests = Gauge(
        'ai_concurrent_requests',
        '当前并发请求数'
    )
    
    # Gauge: 向量库连接池状态
    vector_db_pool = Gauge(
        'ai_vector_db_pool',
        '向量库连接池状态',
        ['state']  # state: active, idle, failed
    )
    
    # 自定义指标:RAG召回质量
    rag_recall_score = Gauge(
        'ai_rag_recall_score',
        'RAG召回质量评分',
        ['doc_type']
    )


def track_request(endpoint: str):
    """装饰器:自动记录请求指标"""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # 增加并发计数
            AIMetrics.concurrent_requests.inc()
            start_time = time.time()
            
            try:
                result = await func(*args, **kwargs)
                status = "success"
                return result
            except Exception as e:
                status = "error"
                raise
            finally:
                # 记录指标
                duration = time.time() - start_time
                AIMetrics.request_total.labels(endpoint=endpoint, status=status, model="gpt-4").inc()
                AIMetrics.request_duration.labels(endpoint=endpoint, model="gpt-4").observe(duration)
                AIMetrics.concurrent_requests.dec()
        return wrapper
    return decorator


# 暴露监控端点
@app.get("/metrics")
async def metrics():
    """Prometheus监控指标端点"""
    return Response(generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST)

5.2 日志系统:结构化日志

import structlog
from structlog.processors import JSONRenderer, TimeStamper, StackInfoRenderer
import logging

# 配置结构化日志
structlog.configure(
    processors=[
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        TimeStamper(fmt="iso"),
        StackInfoRenderer(),
        JSONRenderer()
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
    wrapper_class=structlog.stdlib.BoundLogger,
    cache_logger_on_first_use=True,
)

logger = structlog.get_logger()

# 使用示例
async def chat_with_logging(req: ChatRequest, user_id: str):
    """带结构化日志的问答处理"""
    
    # 绑定上下文
    log = logger.bind(
        trace_id=uuid.uuid4().hex,
        user_id=user_id,
        session_id=req.session_id,
        question_len=len(req.question)
    )
    
    log.info("chat_request_started", question_preview=req.question[:50])
    
    try:
        start_time = time.time()
        # ... 处理逻辑 ...
        duration = time.time() - start_time
        
        log.info("chat_request_completed", 
                 duration_ms=duration * 1000,
                 tokens_used=150,
                 confidence=0.92)
        
        return response
        
    except TimeoutError as e:
        log.error("chat_request_timeout", 
                  timeout_seconds=30,
                  error_type=str(type(e).__name__))
        raise
        
    except Exception as e:
        log.exception("chat_request_failed", 
                      error=str(e),
                      error_type=str(type(e).__name__))
        raise

5.3 告警规则

# Prometheus告警规则
groups:
  - name: ai_service_alerts
    interval: 30s
    rules:
    # 错误率告警
    - alert: AIRequestErrorRateHigh
      expr: |
        (sum(rate(ai_request_total{status="error"}[5m])) / 
         sum(rate(ai_request_total[5m]))) > 0.05
      for: 2m
      labels:
        severity: critical
      annotations:
        summary: "AI服务错误率过高"
        description: "AI服务5分钟错误率超过5%,当前值: {{ $value }}"
    
    # 延迟告警
    - alert: AIRequestLatencyHigh
      expr: |
        histogram_quantile(0.95, 
          sum(rate(ai_request_duration_seconds_bucket[5m])) by (le, endpoint)
        ) > 5
      for: 3m
      labels:
        severity: warning
      annotations:
        summary: "AI服务响应延迟过高"
        description: "P95延迟超过5秒,当前值: {{ $value }}s"
    
    # 并发数告警
    - alert: AIConcurrentRequestsHigh
      expr: ai_concurrent_requests > 50
      for: 1m
      labels:
        severity: warning
      annotations:
        summary: "AI服务并发请求数过高"
        description: "当前并发数: {{ $value }}"
    
    # 向量库连接池告警
    - alert: AIVectorDBPoolExhausted
      expr: ai_vector_db_pool{state="active"} < 1
      for: 30s
      labels:
        severity: critical
      annotations:
        summary: "向量库连接池已耗尽"
        description: "无可用连接,服务可能无法处理检索请求"

06 全链路:从接口到部署再到监控的完整闭环

以上各个环节不是孤立的,它们共同构成了一个完整的工程化闭环:

需求分析 → 接口设计(API/模型) → 业务建模(状态机/数据) 
   → 服务开发(代码) → 容器化(Docker) → 编排部署(K8s) 
   → 监控(Prometheus/Grafana) → 告警(AlertManager) 
   → 日志(ELK) → 运维反馈 → 迭代优化 → 回到接口设计

全栈AI工程师就是这个闭环的掌控者。

07 写在最后:全栈是态度,不是要求

很多人听到"全栈"两个字就退缩了,觉得自己不可能什么都懂。

但全栈AI工程师的核心竞争力,不是"什么都精通",而是"从接口到部署到监控的完整链路你都走过、都理解、都知道坑在哪里"。

你不需要写出全世界最优雅的K8s配置文件,但你得知道Pod起不来了该查哪个日志。

你不需要成为Prometheus专家,但你得知道哪些指标告警了意味着系统快挂了。

你不需要前端写得比专业前端还好,但你得理解SSE流式响应为什么会被Nginx截胡,以及怎么解决。

这就是我们要找的人:不只会写代码,更懂从接口到容器、从建模到监控的完整链路。


简历投递:recruit@ai-engineering.com
标题请注明:【全栈AI】+ 姓名

我们不要求你什么都精通,但我们希望你对整条链路充满好奇和掌控欲。

更多推荐