关于AI系统架构的几点记录

按照传统思维,大致就是这么几个组件:

  1. 文档处理模块:负责把各种格式的文档转换成文本切片
  2. 向量数据库:存储文档的向量表示
  3. LLM 调用层:负责理解和生成
  4. 业务逻辑层:处理用户请求、路由、鉴权等

看架构图也很漂亮:

一开始的设想其实挺简单

当时的场景是做一个企业知识库问答系统,需求听起来不难:用户提问,系统从文档里找答案,用 LLM 组织语言返回。按照传统思维,大致就是这么几个组件:

  1. 文档处理模块:负责把各种格式的文档转换成文本切片
  2. 向量数据库:存储文档的向量表示
  3. LLM 调用层:负责理解和生成
  4. 业务逻辑层:处理用户请求、路由、鉴权等

看架构图也很漂亮:

graph TD A[用户请求] --> B[API 网关] B --> C[业务逻辑层] C --> D[向量检索] C --> E[LLM 调用] D --> F[向量数据库] E --> G[LLM 模型] F --> H[文档存储]

图很清楚,逻辑也顺。但第一次真正跑起来就发现了问题:这个架构把每个组件都当成了"可靠的服务调用",实际上 AI 组件压根不按这个剧本演。

第一个坑:把 LLM 当成普通 HTTP 服务

一开始的代码是这么写的(Python 为例):

import httpx

async def query_llm(prompt: str) -> str:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://api.openai.com/v1/chat/completions",
            headers={"Authorization": f"Bearer {API_KEY}"},
            json={
                "model": "gpt-4",
                "messages": [{"role": "user", "content": prompt}],
                "timeout": 30
            }
        )
        return response.json()["choices"][0]["message"]["content"]

这个函数在测试环境跑了几天都挺好,直到某天早上并发量突然翻倍。系统开始疯狂报错,大部分都是 TimeoutConnection 错误。

问题很明显:我把 LLM 调用当成了普通的 HTTP 请求,但实际上 LLM 服务有几个很明显的特点:

  1. 延迟不稳定:同样的请求,有时 500ms 返回,有时要 5 秒
  2. 并发限制严格:大多数提供商都有严格的 rate limit
  3. 成本随用量线性增长:失败重试要真金白银

当时的修复方案是加了一层调用管理:

class LLMCaller:
    def __init__(self, max_concurrent=5, timeout=30):
        self.semaphore = asyncio.Semaphore(max_concurrent)
        self.timeout = timeout

    async def call(self, prompt: str, retry=3) -> str:
        for attempt in range(retry):
            async with self.semaphore:
                try:
                    async with asyncio.timeout(self.timeout):
                        # 实际调用逻辑
                        result = await self._do_call(prompt)
                        return result
                except asyncio.TimeoutError:
                    if attempt == retry - 1:
                        raise
                    await asyncio.sleep(2 ** attempt)  # 指数退避
                except httpx.HTTPStatusError as e:
                    if e.response.status_code == 429:  # Rate limit
                        wait_time = int(e.response.headers.get("Retry-After", 60))
                        await asyncio.sleep(wait_time)
                        continue
                    raise

加了这层之后,稳定性确实好多了,但让我意识到一个更深层的问题:AI 组件的集成不能按传统微服务那套来做,必须要考虑其独特的不确定性。

第二个坑:向量检索和 LLM 的边界问题

一开始的设计是:先做向量检索,把 top-k 个文档片段喂给 LLM,让它组织答案。听起来很合理。

但实际跑起来发现几个问题:

  1. 检索质量参差不齐:有时候检索结果很差,LLM 拿到的是一堆不相关的文档
  2. 上下文长度爆炸:用户问题很简单,但为了确保检索充分,每次都塞了 10+ 个文档片段
  3. 成本不可控:每次请求都要跑一次 LLM,不管检索结果如何

当时的解决方案是加了一个"检索质量评估"组件:

async def enhanced_query(query: str) -> str:
    # 先做快速检索
    docs = await vector_search(query, top_k=3)

    # 评估检索质量
    relevance = await assess_relevance(query, docs)

    if relevance < 0.5:
        # 质量不够,扩展检索
        docs = await vector_search(query, top_k=10)
        relevance = await assess_relevance(query, docs)

        if relevance < 0.3:
            # 还是差,直接走通用回答
            return await generic_llm_response(query)

    # 质量可以,用 LLM 组织答案
    return await llm_summarize(query, docs)

这个方案多了一层判断逻辑,但整体效果明显好转,而且成本也降了约 30%。让我认识到:AI 系统里的组件之间不是简单的串联,而是需要有"中间层"来做质量评估和路由决策。

第三个坑:状态管理和会话一致性

用户开始反馈"为什么问同样的问题,答案每次都不一样"。这是 AI 系统常见的问题:每次请求都是独立的,没有状态管理。

一开始我想了个简单的方案:把每次对话历史存到 Redis 里,下次请求时把历史一起塞给 LLM。代码大概是这样:

async def chat_with_history(user_id: str, message: str) -> str:
    history = await redis.get(f"chat:{user_id}")
    if history:
        messages = json.loads(history)
    else:
        messages = []

    messages.append({"role": "user", "content": message})

    response = await llm_call(messages)
    messages.append({"role": "assistant", "content": response})

    await redis.set(f"chat:{user_id}", json.dumps(messages))
    return response

这个方案跑了几天就崩了:Redis 内存爆炸,而且 LLM 的上下文长度限制很快就到了。

实际的修复方案是做了两层优化:

  1. 对话摘要:每过几轮对话,就把前面的历史用 LLM 总结一下,减少上下文长度
  2. 分级存储:最近几轮放 Redis,历史对话存到数据库,只保留关键信息
async def chat_with_memory(user_id: str, message: str) -> str:
    # 获取最近对话
    recent_history = await redis.get(f"chat:recent:{user_id}")
    messages = json.loads(recent_history) if recent_history else []

    # 检查是否需要摘要
    if len(messages) > 10:
        summary = await llm_summarize_history(messages[:5])
        messages = messages[5:]
        messages.insert(0, {"role": "system", "content": f"对话摘要:{summary}"})

    messages.append({"role": "user", "content": message})
    response = await llm_call(messages)
    messages.append({"role": "assistant", "content": response})

    # 只保留最近对话
    await redis.set(f"chat:recent:{user_id}", json.dumps(messages[-10:]))
    return response

这个方案虽然复杂了一点,但至少解决了内存和长度限制的问题。不过也让我意识到:AI 系统的状态管理比传统系统复杂,需要考虑模型的上下文限制和成本。

调整后的架构

经过这些折腾,调整后的架构图变成了这样:

graph TD A[用户请求] --> B[API 网关] B --> C[业务逻辑层] C --> D[调用管理器] C --> E[会话管理器] D --> F[向量检索] D --> G[LLM 调用] F --> H{质量评估} H -->|质量好| I[LLM 组织答案] H -->|质量差| J[通用回答] E --> K[Redis + 数据库] K --> L[对话摘要] G --> M[并发控制] G --> N[重试机制]

这个架构虽然看起来复杂了一点,但每一层都是因为实际踩过的坑才加上去的。而且每个组件的职责都比较清晰:

  • 调用管理器:处理 LLM 调用的并发、重试、超时
  • 会话管理器:处理对话历史、摘要、分级存储
  • 质量评估:在检索和 LLM 之间做质量判断
  • 并发控制:控制 LLM 调用的并发数

一些经验总结

这次实践中积累的几个判断,可能会对后面做 AI 系统的人有点用:

  1. 不要把 AI 组件当成可靠服务:它们的延迟、成功率都和传统服务不一样,要做专门的适配层
  2. 组件之间需要"质量评估层":特别是在检索和生成之间,不是简单的串联就行
  3. 成本控制要内置到架构里:LLM 调用是按次付费的,失败重试要考虑成本
  4. 状态管理要考虑上下文限制:不是所有历史都能塞给模型,要做摘要和分级存储
  5. 监控和日志很重要:AI 组件的不确定性,导致问题定位比传统系统难,需要有详细的调用日志

还在持续改进的地方

目前的架构虽然能用了,但还有一些问题没完全解决:

  1. 模型切换:不同场景用不同模型(有些用 GPT-4,有些用 GPT-3.5),现在还是硬编码
  2. 成本预警:用量突增时缺少预警机制,成本控制不够精细
  3. 缓存策略:相似问题的缓存做得还不够,浪费了不少调用量
  4. A/B 测试:新的组件或策略上线前,缺少自动化测试流程

这些都在逐步改进中,但每一项都不是简单的技术问题,需要结合业务和成本来权衡。

最后说一点感受

做 AI 系统架构和传统系统最大的不同,可能就是你不能假设所有组件都是"听话"的。它们有自己的脾气,有自己的限制,还有自己的成本模型。

传统系统设计时,你关注的是"如何让组件之间高效协作";AI 系统设计时,你还要考虑"如何让不确定的组件协作得尽量稳定"。

这不是一个简单的技术问题,更多是对系统复杂性的重新理解。可能也是为什么很多 AI 系统,初版看起来简单,但真正要落地到生产环境,反而要花更多时间在架构优化上。

不过好在,坑踩多了,总能总结出一些可以复用的模式。希望这篇文章的经验,能帮后来者少走一点弯路。


参考:

  • OpenAI API 官方文档:https://platform.openai.com/docs/
  • LangChain 文档:https://python.langchain.com/
  • 向量数据库对比:https://www.pinecone.io/learn/vector-database/

版权声明: 本文首发于 指尖魔法屋-关于AI系统架构的几点记录https://blog.thinkmoon.cn/post/217-ai-system-architecture-components-integration/) 转载或引用必须申明原指尖魔法屋来源及源地址!