1. 为什么现在谈 AI Native 架构正当时
过去两年,我参与过三个从零起步的 AI 项目,也接手过两个“传统系统加挂 AI 模块”的改造项目。这两类项目的体感差异非常大:前者像在高速公路上边跑边换轮胎,后者像给一栋老楼加装电梯——结构上处处受限,最后往往只能做到“能用”,离“好用”差得很远。这也是我越来越坚定一个判断的原因:AI Native 不是把 AI 塞进系统,而是让系统从第一天起就围绕 AI 的能力边界和运行特征来设计。
所谓 AI Native 架构,核心就一句话:把模型推理、上下文管理、工具调用、记忆与反馈回路当作系统的一等公民,而不是把它们当成某个业务模块里的一个函数调用。它解决的问题很具体——传统架构里,业务逻辑是确定的、状态是显式的、延迟是可预测的;而 AI 系统里,输出是概率性的、上下文是动态膨胀的、延迟是波动的、成本是按 token 计费的。这两套世界观硬拼在一起,就会出现提示词散落在业务代码里、会话状态和数据库事务打架、模型换了要改十几个文件这类典型症状。
这篇文章适合三类人看:一是准备从零搭一个 AI 应用的开发者,二是正在做传统系统 AI 化改造的架构师,三是对 Agent、RAG、LLM 编排感兴趣但还没动手的工程师。我会把架构分层、核心模块、实操步骤、参数取舍和踩过的坑都摊开讲,尽量做到你读完能直接照着搭一个最小可用的骨架。
2. AI Native 架构的整体分层与设计思路
2.1 从“业务为中心”到“能力为中心”的思维切换
传统分层架构通常是 Controller → Service → DAO,业务逻辑是主干,数据是支撑。AI Native 架构里,我更倾向于把系统看成一条能力流水线:输入进来先经过理解层(意图识别、实体抽取),再进入编排层(决定是直接回答、检索增强还是调用工具),然后到执行层(模型推理、工具执行),最后经过后处理层(校验、格式化、落库)。业务逻辑不再是一条写死的 if-else 链,而是编排层根据上下文动态决定的路径。
这个切换带来的最大好处是可替换性。当模型从 A 换成 B,或者从纯对话换成带工具调用的 Agent,你只需要改编排层的策略配置,而不是翻遍整个代码库。我实测过一个项目,把底层模型从一个小参数模型换成另一个厂商的大模型,改动量控制在两个配置文件加一个适配器类,半天完成灰度。
2.2 四层架构的职责划分
我习惯把 AI Native 系统拆成四层,每层职责单一、边界清晰:
| 层级 | 核心职责 | 典型组件 | 不该做的事 |
|---|---|---|---|
| 接入层 | 协议适配、鉴权、限流 | API 网关、SSE 通道 | 不碰业务逻辑和提示词 |
| 编排层 | 意图路由、流程控制、上下文组装 | 编排引擎、状态机 | 不直接调模型 SDK |
| 能力层 | 模型推理、工具执行、检索 | 模型适配器、工具注册表、向量库 | 不感知具体业务场景 |
| 数据层 | 会话存储、向量存储、审计日志 | 关系库、向量库、对象存储 | 不做业务判断 |
这么分的原因很实际:能力层是最容易变的,模型版本、工具接口、检索策略几乎每周都在动;而数据层的 schema 一旦定下来就尽量别动。把易变的部分隔离在上层,把稳定的部分沉在下层,是控制维护成本的关键。我见过太多项目把提示词硬编码在 Service 里,结果一次提示词优化要发一次全量版本,回滚还特别麻烦。
2.3 为什么不用微服务一把梭
有朋友会问,那是不是每个能力都拆成微服务?我的经验是初期不要。AI 系统的调用链本来就长,一次请求可能涉及意图识别、检索、重排、生成、校验五六个步骤,如果每步都是跨网络调用,光是网络抖动和序列化开销就能把 P99 延迟推到不可接受。我建议初期用模块化单体,把编排层和能力层放在同一个进程里,用清晰的接口隔离,等某个能力真的成为瓶颈(比如向量检索 QPS 打满)再单独拆出去。这个判断标准很简单:当某个模块的扩容需求和主进程不一致时,才拆。
3. 核心模块的细节拆解与实操要点
3.1 模型适配器:把模型当成可插拔的零件
模型适配器是整个架构里最值得先写好的东西。它的职责是屏蔽不同模型厂商的 API 差异,对上暴露统一的接口。我通常定义三个方法:chat(messages, tools)、embed(texts)、count_tokens(text)。前两个好理解,第三个经常被忽略,但它直接决定了你的上下文裁剪策略和成本预估。
写适配器时有几个坑要避开。第一,不要假设所有模型都支持 function calling,有些模型只支持纯文本,这时候工具调用要靠提示词模拟,适配器里要做能力探测。第二,流式输出的分片格式各家不同,有的按字符,有的按 token,有的会在最后补一个 usage 字段,适配器要统一成一种事件格式再往上抛。第三,错误码要归一化,限流、超时、内容过滤这三类错误在上层要能区分处理,不能笼统抛一个 Exception。
class ModelAdapter: def __init__(self, config): self.model = config["model"] self.timeout = config.get("timeout", 30) self.max_retries = config.get("max_retries", 2) def chat(self, messages, tools=None, stream=False): # 统一入参格式,内部做厂商差异适配 payload = self._build_payload(messages, tools) for attempt in range(self.max_retries + 1): try: return self._invoke(payload, stream) except RateLimitError: time.sleep(2 ** attempt) except ContentFilterError as e: return self._safe_fallback(e)3.2 上下文管理:比模型选型更影响体验
很多人把精力全花在选模型上,结果上线后发现体验差,问题往往出在上下文管理。AI Native 系统里,上下文不是简单地把历史消息拼起来,而是要做分层管理:系统提示词(稳定)、长期记忆(半稳定)、近期对话(易变)、检索结果(按需注入)。这四类内容的生命周期完全不同,混在一起管理必然出问题。
我的做法是给每类内容打上标签和优先级,组装上下文时按优先级填充,超出预算就从低优先级开始裁剪。预算怎么算?假设模型上下文窗口是 128K token,我会预留 20% 给输出,剩下 80% 里再留 10% 给工具返回结果,实际可用于输入的约 90K。这个数字不是拍脑袋,是因为工具返回的内容长度不可控,留足余量能避免请求直接被拒。
提示:裁剪上下文时优先保留系统提示词和最近三轮对话,中间的检索结果可以按相关性分数截断。千万别用简单的“保留最近 N 条”,那样会丢掉关键的系统约束。
3.3 工具注册与调用:让模型安全地“动手”
Agent 架构里,工具调用是最容易出安全事故的地方。我的原则是工具必须显式注册、参数必须强校验、执行必须有沙箱。注册表里每个工具要声明名称、描述、参数 schema、超时时间和是否需要人工确认。模型返回的工具调用请求,先过 schema 校验,再判断权限,最后才执行。
参数校验这块,我强烈建议用 JSON Schema 而不是手写 if-else。模型偶尔会返回缺字段或者类型不对的参数,Schema 校验能在执行前就拦下来,避免脏数据进到下游。对于写操作类工具(比如发消息、改数据),我一般会加一道确认机制:模型生成调用意图后,先返回给用户确认,确认通过才真正执行。这个设计在早期能挡掉大量误操作。
3.4 记忆与状态:会话不是一条直线
传统 Web 系统的会话状态通常存在 Redis 里,key 是 session_id,value 是一个扁平的对象。AI 系统的会话要复杂得多:它可能有多条分支(用户中途切换话题)、有摘要(长对话压缩)、有实体槽位(记住用户提到的关键信息)。我一般用事件溯源的思路来存会话:每次交互追加一条事件记录,当前状态由事件回放得出。这样既能支持分支,又方便做审计和回放调试。
摘要的触发时机也有讲究。我试过按轮次触发(每 10 轮压缩一次)和按 token 触发(超过阈值压缩),实测下来按 token 触发更稳,因为不同轮次的内容长度差异太大。压缩时用一个小模型做摘要,成本低且够用,摘要结果要保留原始事件的引用 ID,方便需要时回溯细节。
4. 从零搭建的完整实操流程
4.1 环境准备与依赖选型
假设你要搭一个最小可用的 AI Native 服务,我建议的技术栈是这样:Python 3.11 起步(异步生态成熟),Web 框架用 FastAPI(原生支持异步和 SSE),向量库初期用轻量的本地方案(比如基于文件的索引),关系库用 PostgreSQL(JSONB 字段存会话事件很方便),缓存用 Redis。模型侧先接一个通用对话模型加一个 embedding 模型即可。
依赖安装没什么特别的,但有两个细节要注意。一是锁定版本,AI 生态的库更新极快,不锁版本今天能跑明天就崩。二是把模型 SDK 的调用封装在适配器里,业务代码不直接 import 厂商 SDK,这样换厂商时改动面最小。
pip install fastapi uvicorn httpx pydantic psycopg[binary] redis pip freeze > requirements.txt4.2 编排引擎的最小实现
编排引擎的核心是一个决策循环:拿到用户输入后,先判断意图,再决定走哪条路径,执行后判断是否需要继续循环(比如工具调用后要再生成一次回答)。我用状态机来实现,状态包括UNDERSTAND、RETRIEVE、PLAN、ACT、RESPOND,每个状态有明确的进入条件和退出条件。
class Orchestrator: def run(self, session_id, user_input): ctx = self.context_manager.load(session_id) ctx.append_user(user_input) state = "UNDERSTAND" while state != "DONE": if state == "UNDERSTAND": intent = self.understand(ctx) state = "RETRIEVE" if intent.needs_knowledge else "PLAN" elif state == "RETRIEVE": docs = self.retriever.search(ctx.last_query) ctx.inject_docs(docs) state = "PLAN" elif state == "PLAN": plan = self.planner.decide(ctx) state = "ACT" if plan.has_tool_call else "RESPOND" elif state == "ACT": result = self.tool_executor.run(plan.tool_call) ctx.append_tool_result(result) state = "PLAN" # 回到规划,判断是否还要继续 elif state == "RESPOND": answer = self.generator.generate(ctx) ctx.append_assistant(answer) state = "DONE" self.context_manager.save(session_id, ctx) return ctx.last_answer这个循环里最关键的是最大迭代次数限制。我一般设 5 次,超过就强制进入 RESPOND,用当前已有信息生成回答。没有这个限制,模型可能在工具调用和规划之间无限循环,既烧钱又拖延迟。
4.3 检索增强的落地细节
RAG 这块我踩过的坑最多。第一个坑是切分粒度,切太碎语义不完整,切太大检索不准。我的经验是按语义段落切,每段控制在 300 到 500 token,相邻段落保留 50 token 重叠。第二个坑是只做向量检索,实测下来纯向量检索对关键词类查询效果一般,我一般会加一路关键词检索,两路结果做融合排序。
融合排序用 RRF(倒数排名融合)就够了,不需要上复杂的重排模型。RRF 的公式很简单:每个文档的得分是它在各路检索中排名的倒数之和。这个方法的优点是不需要调参,对两路检索的分数尺度不敏感,实测效果稳定。
| 检索策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 纯向量 | 语义相似查询 | 泛化好 | 关键词命中差 |
| 纯关键词 | 精确术语查询 | 准确 | 无法处理同义 |
| 混合+RRF | 通用场景 | 兼顾两者 | 实现稍复杂 |
4.4 可观测性:没有日志的 AI 系统等于裸奔
AI 系统的调试难度远高于传统系统,因为同样的输入可能得到不同的输出。所以全链路日志是刚需。我一般记录这几类信息:每次请求的完整上下文(脱敏后)、模型返回的原始响应、工具调用的入参和结果、每步的耗时和 token 消耗。这些数据存到关系库里,用 JSONB 字段存结构化部分,方便后续查询和分析。
成本监控也要从第一天就做。按 token 计费的模型,一次请求的成本可能从几分钱到几块钱不等,不做监控很容易月底收到账单才发现异常。我的做法是在适配器层统一记录 token 用量,按会话、按用户、按天聚合,设一个阈值告警。
5. 常见问题与排查技巧实录
5.1 输出不稳定怎么排查
模型输出不稳定是最常见的问题,表现是同样的输入有时答得好有时答得差。排查顺序我一般是这样的:先看温度参数,如果设得偏高(比如 0.8 以上),先降到 0.2 试试;再看上下文里有没有冲突信息,比如检索结果和系统提示词矛盾,模型会摇摆;最后看提示词结构,把关键约束放在开头和结尾,中间放参考资料,这个位置效应实测有效。
还有一个隐蔽的原因是上下文超长被静默截断。有些模型在超长时会悄悄丢掉中间部分,导致关键信息丢失。解决办法是在适配器里主动做 token 计数,超预算就按优先级裁剪,而不是依赖模型自己处理。
5.2 工具调用失败的典型原因
工具调用失败我整理过一张速查表,覆盖了八成以上的情况:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 模型不调用工具 | 工具描述不清 | 检查 description 是否说明用途和时机 |
| 参数缺字段 | schema 太宽松 | 加 required 约束 |
| 参数类型错 | 模型理解偏差 | 在描述里给示例值 |
| 调用超时 | 工具本身慢 | 加超时和降级 |
| 重复调用 | 循环没退出条件 | 加最大迭代限制 |
注意:工具描述里一定要写清楚“什么时候用”和“什么时候不用”,只写功能说明模型经常在不该调用的时候调用。
5.3 延迟优化的几个实用手段
AI 系统的延迟大头在模型推理,但也不是没有优化空间。我常用的手段有:流式输出让用户先看到部分结果,感知延迟大幅降低;并行化独立的检索和意图识别;缓存高频问题的回答和 embedding 结果;小模型前置用便宜快的小模型做意图识别和路由,只把复杂请求交给大模型。这几个手段叠加,实测能把 P95 延迟压下来一半左右。
5.4 成本控制的经验
成本控制的核心是分级处理。不是所有请求都需要大模型,简单问答用小模型,复杂推理才升级。检索结果做去重和截断,别把十篇文档全塞进去。摘要用便宜模型,生成用贵模型。我还会给每个用户设一个日 token 预算,超了就降级到小模型或者排队,避免单个用户把额度吃光。
6. 我踩过的几个印象深刻的坑
第一个坑是把会话状态存在内存里。早期图省事用进程内字典存会话,单机测试没问题,一上多实例就出现会话丢失和串号。后来改成 Redis 加数据库双写,Redis 做热数据,数据库做持久化,才稳定下来。
第二个坑是提示词版本管理混乱。提示词改了没记录,出问题不知道回滚到哪版。后来我把提示词抽成独立文件,用版本号管理,每次改动记录变更原因和效果对比,才算可控。
第三个坑是忽略内容安全过滤。模型可能生成不合适的内容,也可能被诱导执行危险操作。我在适配器层加了输入输出双向过滤,工具执行前加权限校验,虽然增加了一点延迟,但这是必须的底线。
第四个坑是过早优化架构。一开始就上微服务、上消息队列、上复杂的编排框架,结果业务还没跑通,光维护基础设施就耗掉大半精力。后来我坚持先单体跑通闭环,再按瓶颈拆,效率高很多。
7. 后续可以这样扩展
这套骨架跑通之后,扩展方向其实很多。想做多 Agent 协作,可以在编排层加一个 Agent 注册表,让不同 Agent 负责不同领域,由一个协调者分发任务。想做个性化,可以在记忆层加用户画像,把长期偏好注入上下文。想做离线评估,可以把线上请求采样存下来,定期用新模型跑一遍对比效果。这些扩展都不需要推翻现有架构,因为它们都建立在清晰的分层边界之上——这也是我一开始坚持分层的原因,好的架构不是预测未来,而是让未来容易改。