1. 为什么现在必须重新思考系统架构
过去两年我参与过三个从零起步的 AI 项目,也接手过两个“传统系统加个模型接口”的改造项目。踩过的坑让我越来越确信一件事:把大模型当成一个外部 API 塞进现有架构,和真正以 AI 为核心重新设计系统,是两种完全不同的工程实践。前者像是在老房子里加装一台新电器,后者则是从地基开始就为这台电器预留电路、散热和空间。
AI Native 架构要解决的核心问题,不是“怎么调用模型”,而是“当模型成为系统的一等公民之后,数据怎么流、状态怎么管、失败怎么兜、成本怎么控”。它适合正在做 AI 应用从 0 到 1 的开发者、需要把 AI 能力嵌入业务系统的架构师,以及想理解 Agent 和 LLM 工程化落地路径的技术负责人。如果你只是偶尔调一下模型接口做个 Demo,这篇文章的部分内容可能偏重;但只要你打算让 AI 能力长期稳定地跑在生产环境里,下面这些经验应该能帮你少走几个月弯路。
我先把结论放在前面:AI Native 架构的本质,是把不确定性当作系统的基本属性来设计,而不是当作异常来处理。传统后端追求确定性——同样的输入必须得到同样的输出;AI 系统的输出天然带概率,所以架构的每一层都要为“可能不一样”做好准备。这个思维转变,是后面所有具体设计决策的源头。
2. 核心思路拆解:AI Native 到底新在哪里
2.1 从“模型调用”到“模型编排”的认知升级
很多人对 AI 架构的第一印象是:前端发请求,后端拼 Prompt,调一次 LLM,拿到结果返回。这个链路在 Demo 阶段没问题,但一旦业务复杂起来就会崩。原因很简单——真实任务往往需要多步推理、外部工具调用、结果校验和重试,单次调用根本覆盖不了。
我习惯把 AI Native 架构分成三个层次来理解。最底层是模型层,负责推理能力,可能是云端 LLM,也可能是本地小模型。中间层是编排层,这是 AI Native 和传统架构最大的区别所在,它管理 Prompt 模板、上下文组装、工具调用、多步流程和状态机。最上层是应用层,面向具体业务场景,比如客服、写作、数据分析。
编排层为什么关键?因为它是把“不确定的模型输出”转化为“可用的业务结果”的翻译器。举个实际例子:用户问“帮我查一下上个月的销售异常”。传统做法是写死 SQL 查询逻辑;AI Native 做法是让模型先理解意图,决定调用哪个数据工具,拿到数据后再让模型分析异常点,最后生成自然语言解释。这中间每一步都可能失败或跑偏,编排层的职责就是让这个流程可控、可观测、可恢复。
2.2 为什么不能直接套用微服务那一套
有微服务经验的同学容易犯一个错:把每个 AI 能力拆成一个独立服务,然后按传统 RPC 方式互相调用。我早期也这么干过,结果发现两个问题。第一,AI 服务的响应时间波动极大,同一个接口可能 200ms 返回,也可能 8 秒才返回,传统的超时和重试策略完全不适用。第二,AI 服务之间往往需要共享大量上下文,拆得太细会导致上下文在服务间反复传递,既浪费 token 又容易丢失信息。
所以我的建议是:AI Native 架构的拆分粒度应该按“任务边界”而不是“技术边界”来定。一个完整的 Agent 任务,从意图识别到工具调用到结果生成,最好放在同一个编排单元里,内部用函数调用而不是网络调用来串联。只有那些真正独立、可复用的能力(比如向量检索、文档解析)才值得拆成单独服务。
2.3 Agent 和普通 LLM 调用的本质区别
热词里反复出现 Agent,这里必须说清楚。普通 LLM 调用是“一问一答”,Agent 是“给一个目标,自己决定怎么一步步完成”。区别体现在三个地方:Agent 有循环,会反复思考-行动-观察直到任务完成;Agent 有工具,能主动调用外部能力;Agent 有记忆,会在多轮交互中维护状态。
这个区别直接决定了架构复杂度。普通调用只需要一个请求-响应通道,Agent 需要一个完整的运行时环境,包括任务队列、状态存储、工具注册表和循环控制逻辑。我见过太多项目号称做 Agent,实际上只是把 Prompt 写长了一点,根本没有循环和工具调用,这种本质上还是普通调用。
3. 核心模块的详细设计与实操要点
3.1 上下文管理:AI 系统的“内存”
上下文管理是我认为 AI Native 架构里最容易被低估、也最容易出问题的部分。LLM 的上下文窗口有限,而真实业务往往需要注入大量信息:系统指令、历史对话、检索到的文档、工具返回结果。怎么在有限窗口里塞进最有用的信息,直接决定输出质量。
我的做法是分层管理上下文。系统层放固定指令和角色设定,这部分基本不变。会话层放最近几轮对话,按时间倒序保留。检索层放从知识库召回的文档片段,按相关度排序。工具层放工具调用结果,用完即弃。每一层都有独立的预算,比如系统层不超过 500 token,检索层不超过 2000 token,超出就按优先级裁剪。
这里有个实操技巧:不要等上下文满了才裁剪,而是在组装阶段就做预算控制。我通常会给每层设一个硬上限,组装时如果超出就触发压缩或丢弃。压缩可以用模型自己做摘要,但要注意摘要本身也消耗 token,所以只对长文档做摘要,短内容直接截断更划算。
注意:上下文裁剪一定要保留“最近一轮用户输入”和“系统核心指令”,这两部分丢了会导致答非所问或角色崩坏。我踩过一次坑,裁剪逻辑把系统指令裁掉了,结果模型开始用完全不同的语气回答,排查了半天才发现是上下文问题。
3.2 工具调用与函数注册的工程细节
Agent 能干活靠的是工具。工具调用的工程实现有几个关键点。第一是工具描述的质量,模型靠描述来决定调不调、怎么调。描述要写清楚功能、参数含义和适用场景,我一般会写三段:一句话功能概述、每个参数的说明、一个调用示例。第二是参数校验,模型生成的参数经常有格式错误,必须在执行前校验,校验失败要把错误信息返回给模型让它重试。
第三是工具执行的隔离。工具可能访问数据库、调用外部 API、执行代码,这些操作必须沙箱化。我的做法是每个工具定义明确的权限边界,比如只读工具不能有写操作,外部调用必须设超时和熔断。第四是结果格式化,工具返回的原始数据往往很长,直接塞回上下文会爆窗口,需要先做摘要或结构化提取。
下面是一个工具注册的简化示例,用 Python 字典描述工具元信息:
tools = [ { "name": "query_sales", "description": "查询指定时间范围的销售数据。用于回答销售相关问题时获取原始数据。", "parameters": { "start_date": "开始日期,格式 YYYY-MM-DD", "end_date": "结束日期,格式 YYYY-MM-DD" }, "example": "query_sales(start_date='2024-01-01', end_date='2024-01-31')" } ]这个结构看起来简单,但描述写得好不好,直接决定模型调用准确率。我实测下来,加了 example 字段之后,参数格式错误率能降一半以上。
3.3 状态存储与任务持久化
Agent 任务可能跑很久,中间还要等工具返回,所以状态必须持久化。我用过两种方案:一种是轻量的,用 Redis 存任务状态,适合短任务;另一种是重量的,用数据库存完整执行轨迹,适合需要审计和回溯的场景。
状态里要存什么?至少包括:任务 ID、当前步骤、已完成的步骤、每步的输入输出、当前上下文快照、错误信息。这样任务中断后能从断点恢复,也方便排查问题。我特别建议存完整的执行轨迹,因为 Agent 跑偏的时候,只有回看每一步才能找到是哪一步开始错的。
实操心得:状态存储的写入频率要控制。每步都写数据库会很慢,我的做法是内存里维护状态,关键节点(步骤完成、工具返回、出错)才落盘。这样既保证可恢复,又不拖慢执行。
3.4 失败处理与重试策略
AI 系统的失败模式比传统系统多得多:模型超时、输出格式错误、工具调用失败、上下文超限、内容被安全策略拦截。每种失败的恢复方式都不一样,不能用一个统一的重试逻辑。
我的分类处理策略是这样的。超时类失败直接重试,但要有退避,第一次等 1 秒,第二次等 3 秒,最多重试 3 次。格式类失败把错误信息返回给模型让它重新生成,通常一次就能修正。工具类失败要看工具类型,只读工具可以重试,写操作要谨慎,可能需要人工介入。上下文超限要触发裁剪后重试。安全拦截不能重试,要直接返回用户并记录。
这里有个容易忽略的点:重试要有全局预算。如果每个步骤都允许重试 3 次,一个 5 步的任务最多可能跑 15 次模型调用,成本和延迟都会失控。我一般设一个任务级的总调用次数上限,比如 20 次,超过就终止并返回部分结果。
4. 完整实操流程:从零搭一个最小可用架构
4.1 环境准备与技术选型
先说选型思路。模型层我建议初期直接用成熟的大模型 API,不要一上来就自己部署,除非有明确的数据合规要求。编排层可以用现成框架,也可以自己写,我的经验是如果团队对框架不熟,自己写一个轻量编排器反而更可控,核心逻辑也就几百行。存储层 Redis 加 PostgreSQL 基本够用,Redis 管状态,PostgreSQL 管轨迹和审计。
开发环境需要准备的东西不多:Python 3.10 以上、一个模型 API 的访问凭证、Redis 和 PostgreSQL 实例。我习惯用 Docker Compose 把依赖服务拉起来,这样环境一致性好,换机器不用重新配。
# 启动依赖服务 docker compose up -d redis postgres4.2 编排器的核心循环实现
编排器的核心是一个循环:组装上下文、调用模型、解析输出、决定下一步。如果模型要求调用工具,就执行工具并把结果加回上下文,继续循环;如果模型给出最终答案,就结束。
这个循环的关键在于终止条件要明确。我设了三个:模型返回最终答案、达到最大步数、达到总调用预算。三个条件任一满足就退出。最大步数我一般设 10 步,大部分任务 3 到 5 步就能完成,10 步足够覆盖复杂情况,又能防止死循环。
def run_agent(task, max_steps=10, max_calls=20): context = build_initial_context(task) calls = 0 for step in range(max_steps): if calls >= max_calls: return {"status": "budget_exceeded", "partial": context} response = call_llm(context) calls += 1 if response.is_final: return {"status": "done", "answer": response.content} if response.tool_call: result = execute_tool(response.tool_call) context = append_tool_result(context, result) return {"status": "max_steps", "partial": context}这段代码看起来简单,但每一行背后都有设计考量。比如calls计数放在循环内而不是循环外,是因为一次循环可能触发多次模型调用(比如格式修正)。partial返回是为了让上层能拿到中间结果,不至于全丢。
4.3 上下文组装的具体实现
上下文组装我按前面说的分层来做。先放系统指令,再放最近对话,再放检索结果,最后放当前用户输入。每层组装时检查 token 预算,超了就裁剪。
token 估算不用太精确,按字符数除以 2 粗略估计就够用,中文大概 1 个 token 对应 1.5 到 2 个字符。精确计算反而增加复杂度,收益不大。裁剪时优先丢检索层里相关度最低的,再丢会话层里最老的,系统层和当前输入永远保留。
注意:不同模型对 token 的计算方式不一样,切换模型时预算要重新校准。我有次从一个大窗口模型切到小窗口模型,没改预算,结果频繁触发裁剪,输出质量明显下降。
4.4 工具执行与结果回填
工具执行要包一层统一的封装,处理超时、异常和结果格式化。我的封装逻辑是:先校验参数,再执行,执行时设超时,捕获异常,最后把结果转成模型能理解的格式。
结果回填有个技巧:不要把工具的原始返回直接塞回去,而是做一层转换。比如数据库查询返回的是 JSON 数组,我会转成“查询到 N 条记录,前 3 条是……”这样的自然语言描述,既省 token 又让模型更容易理解。如果结果确实很长,就只回填摘要,完整结果存在状态里,需要时再取。
4.5 可观测性建设
AI 系统不埋点就是黑盒。我至少会记录这几类信息:每次模型调用的输入输出和耗时、每次工具调用的参数和结果、每个任务的完整执行轨迹、错误和重试记录。这些数据一方面用于排查问题,另一方面用于优化 Prompt 和工具描述。
日志格式我建议结构化,用 JSON 存,方便后续分析。关键字段包括任务 ID、步骤序号、事件类型、耗时、token 消耗。有了这些数据,你才能回答“这个月模型成本涨了多少”“哪类任务失败率最高”这类问题。
5. 常见问题与排查技巧实录
5.1 模型输出格式不稳定怎么办
这是最高频的问题。模型有时候返回 JSON,有时候返回带解释的文字,解析经常失败。我的解决办法是双管齐下:一是在 Prompt 里明确要求输出格式,并给一个示例;二是在解析层做容错,先尝试直接解析,失败就用正则提取,再失败就把错误返回给模型让它重新生成。
实测下来,加了输出示例之后,格式正确率能从 70% 提到 90% 以上。剩下 10% 靠解析容错兜底。如果某个场景对格式要求特别严,可以考虑用支持结构化输出的模型接口,但要注意这类接口通常灵活性会差一些。
5.2 Agent 陷入死循环怎么破
死循环的表现是模型反复调用同一个工具,或者反复输出相似内容。原因通常是任务目标不清晰,或者工具返回的结果让模型误以为没完成。排查时先看执行轨迹,找到循环开始的那一步,看模型当时的上下文是什么。
解决手段有三个:一是加最大步数限制,这是兜底;二是在 Prompt 里明确“如果已经获取到足够信息就给出答案”;三是在工具返回里加状态标记,比如“查询已完成,共 N 条结果”,让模型知道这步结束了。我一般三个一起用,效果比较稳。
5.3 成本失控的排查思路
成本突然上涨,通常是三个原因:任务变复杂导致步数增加、上下文变长导致 token 增加、重试变多导致调用次数增加。排查时先看平均步数和平均 token 消耗的变化趋势,定位到是哪类任务出了问题。
优化手段包括:精简系统 Prompt、压缩检索结果、减少不必要的工具调用、给不同任务设不同的预算上限。我还会定期分析哪些 Prompt 片段使用频率低,直接删掉,积少成多能省不少。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 输出格式错误 | Prompt 不明确 | 检查输出要求 | 加示例、加解析容错 |
| 死循环 | 目标不清晰 | 看执行轨迹 | 加步数限制、加完成标记 |
| 成本上涨 | 步数或 token 增加 | 看趋势数据 | 精简 Prompt、设预算 |
| 响应变慢 | 模型或工具慢 | 看耗时分布 | 加超时、换模型、缓存 |
| 答非所问 | 上下文丢失 | 检查组装逻辑 | 保留系统指令和当前输入 |
| 工具调用失败 | 参数错误 | 看工具日志 | 加参数校验、返回错误重试 |
5.5 几个容易忽略的坑
第一个坑是并发下的状态污染。多个任务共享同一个上下文对象时,如果不做隔离,会出现 A 任务的数据跑到 B 任务里。我的做法是每个任务独立上下文,绝不共享可变对象。
第二个坑是模型版本切换。模型升级后行为可能变化,Prompt 需要重新调优。我建议固定模型版本,升级时先在测试环境跑一轮回归。
第三个坑是安全策略误伤。正常业务内容有时会被安全策略拦截,导致任务失败。排查时要区分是模型拒绝还是策略拦截,前者调 Prompt,后者要联系平台确认规则。
第四个坑是工具描述过时。工具改了参数但描述没更新,模型还在按老方式调用。我习惯把工具描述和工具实现放在同一个文件里,改的时候一起改,避免遗漏。
6. 一些关于架构演进的个人体会
这套架构我从第一个版本到现在改了大概七八轮,最大的体会是:不要一开始就追求完美,先让最小闭环跑起来。我最初想设计一个支持多 Agent 协作、动态工具发现、自动 Prompt 优化的复杂系统,结果两个月没跑通一个完整任务。后来退回到单 Agent 加固定工具,一周就跑通了,然后再逐步加功能。
另一个体会是可观测性要早做。我早期没埋点,出了问题只能靠猜,排查一个上下文丢失的 bug 花了两天。后来补上日志,类似问题半小时就能定位。埋点这件事,早做早省心。
还有就是成本意识要贯穿始终。AI 系统的边际成本是随调用量线性增长的,不像传统系统那样边际成本趋近于零。所以每一个设计决策都要问一句:这会不会增加调用次数或 token 消耗?能省的地方一定要省,比如缓存常见问题的答案、复用检索结果、精简 Prompt。
最后分享一个我常用的调试技巧:把每次任务的完整上下文和执行轨迹导出成一个文件,出问题时直接看这个文件,比在日志里翻要快得多。这个文件也是优化 Prompt 的最好素材,看多了自然就知道哪里可以改。