多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AI Native架构实战:从控制流到Agent编排的工程化设计

AI Native架构实战:从控制流到Agent编排的工程化设计 1. 为什么AI Native不是给旧系统加个模型接口这两年AI Native这个词被用得很泛很多团队的理解还停留在在现有系统里接一个大模型 API这个层面。我见过不少项目后端还是那套 CRUD 加定时任务的老结构只是在某个环节塞了一个 LLM 调用然后就对外宣称自己是 AI Native 架构。跑起来之后问题一堆延迟不可控、成本算不清、模型一换整个链路崩、Agent 行为无法复现。这不是 AI Native这只是AI 附加。真正的 AI Native 架构核心判断标准只有一条系统的控制流是不是围绕模型的不确定性来设计的。传统后端假设每个函数调用是确定性的输入 A 必然得到 B所以我们可以用事务、用幂等、用状态机来保证正确性。但 LLM 的输出本质上是概率分布上的采样同样的输入两次调用可能给出不同结果。当你把这种不确定性组件放进一个假设确定性的架构里冲突是必然的。我在实际项目里踩过最典型的一个坑早期做一个文档问答系统把 LLM 调用直接放在同步 HTTP 请求链路里前端等 30 秒拿结果。用户量一上来连接池打满整个服务雪崩。后来才意识到问题不在于模型慢而在于我用同步架构去承载一个本质上是异步、长耗时、可能失败重试的计算任务。这就是架构范式没转过来的代价。所以这篇内容我想聊的是如果你从零开始或者要对现有系统做一次真正的 AI Native 改造架构上到底该怎么想。适合已经有基本后端经验、正在或准备把 LLM、Agent、embedding 这些能力落到生产系统的工程师和架构师。我会把控制流设计、状态管理、检索层、Agent 编排、成本与可观测性这几块拆开讲每一块都给出我实际用过的取舍逻辑而不是罗列概念。2. 把不确定性当成一等公民控制流该怎么重新设计2.1 同步链路是 AI Native 架构的第一个反模式传统 Web 架构里请求-响应是同步的用户点一下服务端算完返回。这套模型对数据库查询、缓存读取都成立因为它们的延迟在毫秒级且方差小。但 LLM 调用的延迟通常在秒级方差极大——同一个 prompt可能 2 秒返回也可能 40 秒超时。把这种组件放进同步链路等于把系统的尾延迟直接暴露给用户。正确的做法是把 LLM 调用抽象成异步任务。用户发起请求后系统立即返回一个任务 ID真正的推理在后台 worker 里跑前端通过轮询或流式推送拿结果。这不是什么新发明本质上是把 LLM 当成一个慢速外部依赖来对待就像处理视频转码、报表生成一样。但这里有个细节很多人忽略流式输出streaming和异步任务不是一回事但可以结合。对于对话类场景用户能接受逐字返回这时候用 SSE 或 WebSocket 做流式是合理的因为首 token 延迟通常只有几百毫秒用户感知是它在思考。但对于批量文档处理、复杂 Agent 任务流式没意义老老实实做异步任务队列。我一般的判断标准是这样的场景类型推荐模式理由实时对话流式 SSE首 token 快用户感知好单次生成文案、摘要异步任务 轮询延迟方差大避免占用连接多步 Agent 任务异步任务 状态查询执行时间长需要中间状态批量处理消息队列 worker吞吐优先可水平扩展2.2 幂等性和重试LLM 调用必须假设会失败传统系统里重试是个需要谨慎对待的操作因为可能造成重复写入。但在 AI Native 系统里重试是常态。模型服务会限流、会超时、会返回格式错误的结果你必须假设每次调用都有相当概率失败。这里的关键设计是把调用模型和消费结果解耦。调用模型这个动作本身应该是幂等的——给定相同的输入和参数重试不应该产生副作用。真正的副作用写数据库、发通知应该发生在结果被验证通过之后。具体怎么做我会给每个 LLM 调用生成一个基于输入内容哈希的request_id把这个 ID 和调用参数、结果一起存下来。重试时先查这个 ID 有没有成功的结果有就直接复用没有才真正发起调用。这样既避免了重复计费也保证了结果的可复现性。import hashlib import json def make_request_id(prompt: str, model: str, params: dict) - str: payload json.dumps({ prompt: prompt, model: model, params: params }, sort_keysTrue) return hashlib.sha256(payload.encode()).hexdigest()[:32] # 调用前先查缓存/记录表 # 命中则直接返回未命中才真正调用模型这个模式我在多个项目里用过效果很实在调试时能精确复现某次调用成本上能砍掉大量重复请求尤其是 Agent 场景里同一个子任务被反复触发的概率很高。2.3 超时和降级给不确定性留出缓冲带LLM 调用必须设置超时而且超时时间要分层。我通常设三层单次调用超时比如 30 秒、单步任务超时比如 2 分钟、整个会话超时比如 10 分钟。任何一层超时都要有明确的降级策略。降级不是简单地返回错误。对于 AI Native 系统降级可以是切换到更小更快的模型、返回缓存的历史相似结果、返回一个正在处理请稍后查看的占位状态。关键是不能让用户面对一个空白或者无限转圈。我踩过的一个坑是早期没做超时分层一个 Agent 任务卡在某个子步骤上整个会话挂了 20 分钟用户早就关页面了但后台 worker 还在跑白白烧钱。后来加了分层超时每个子步骤超时后自动标记失败并让 Agent 决定是重试还是跳过整体可控多了。3. 状态管理Agent 和会话的记忆到底存在哪3.1 无状态服务 外部状态存储是唯一可行解传统后端喜欢无状态服务状态放 Redis 或数据库。AI Native 系统更是如此因为 Agent 的执行可能跨越多个请求、多个 worker甚至多个服务实例。你不可能把会话状态放在某个进程的内存里。但这里的状态比传统系统复杂得多。一个 Agent 会话的状态至少包含对话历史、工具调用记录、中间推理结果、当前执行到哪一步、已经产生的副作用。这些数据的读写模式差异很大——对话历史是追加写、频繁读工具调用记录是结构化查询中间推理结果可能很大且只需要短期保留。我的做法是分层存储热状态当前会话的完整上下文放 Redis设置合理的 TTL比如 2 小时。读写快支持原子操作。温状态历史会话、可追溯的执行记录放关系型数据库或文档数据库用于审计和调试。冷状态embedding 向量、大块文档放专门的向量库或对象存储。这个分层不是拍脑袋定的而是根据访问频率和数据结构选的。热状态要求低延迟高并发Redis 合适温状态要求可查询可追溯Postgres 或 MongoDB 合适冷状态的向量检索有专门的索引结构用 pgvector 或者独立向量库都行。3.2 上下文窗口管理不是塞得越多越好很多人做 AI Native 系统时有个误区既然模型上下文窗口大那就把所有历史都塞进去。结果就是成本飙升、延迟增加而且模型反而被无关信息干扰效果变差。上下文管理的核心是相关性筛选。我的经验是保留最近 N 轮完整对话N 通常 5 到 10更早的历史做摘要压缩同时用 embedding 检索出与当前问题最相关的历史片段插入。这样既控制了 token 量又保留了关键信息。具体实现上我会维护一个上下文预算比如总预算 8000 token其中系统提示占 1000最近对话占 3000检索到的相关历史占 2000留给模型输出的空间 2000。每次组装 prompt 时按这个预算动态裁剪。这个预算要根据实际模型和场景调不是固定值。提示上下文裁剪策略一定要可配置、可观测。我见过因为裁剪逻辑写死导致某些长对话场景直接丢关键信息的案例排查起来非常痛苦。3.3 会话隔离与并发同一个用户的多任务怎么处理一个容易被忽略的问题是同一个用户可能同时发起多个 AI 任务。比如一边让 Agent 查资料一边让另一个 Agent 写代码。如果状态存储设计得不好这两个任务会互相污染。我的做法是以任务为最小状态单元而不是以用户或会话。每个任务有独立的task_id状态挂在task_id下。用户和会话只是任务的归属关系用于权限和展示不用于状态隔离。这样并发任务天然隔离实现上也简单。并发控制上我会对同一个用户的同时活跃任务数做限制比如最多 3 个。超过就排队或者拒绝。这不是技术限制而是产品层面的保护——防止用户误操作或者恶意刷任务把成本打爆。4. 检索层embedding 不是万能药RAG 也不是4.1 embedding 检索的边界在哪embedding 模型把文本映射到向量空间语义相近的文本向量距离近。这个特性让语义检索成为可能但它有明确的边界。第一个边界是精确匹配场景。用户问订单号 12345 的状态embedding 检索可能返回一堆语义相近但订单号不对的文档。这种场景必须用关键词检索或者结构化查询。所以生产系统里混合检索hybrid search几乎是标配embedding 召回 BM25 关键词召回然后融合排序。第二个边界是长文档的粒度问题。把一整篇 5000 字的文档做成一个 embedding检索时粒度太粗返回的内容可能只有一小段相关。正确做法是分块chunking但分块策略很讲究。按固定字数切会切断语义按段落切可能段落太长。我一般用递归分块先按标题切再按段落切最后按句子切保证每块在 200 到 500 token 之间块之间保留一定重叠。第三个边界是embedding 模型的领域适配。通用 embedding 模型在特定领域比如医疗、法律、代码效果会打折。如果预算允许用领域数据微调 embedding 模型召回质量提升很明显。预算不够的话至少要做重排序rerank先用 embedding 粗召回 top 50再用一个 cross-encoder 模型精排 top 5。这一步的性价比极高。4.2 RAG 的常见失败模式RAG检索增强生成听起来简单检索相关文档塞进 prompt让模型基于文档回答。但实际落地时失败模式很多。失败模式一检索到了但模型没用。模型可能忽略检索内容凭自己的知识回答。解决办法是在 prompt 里明确要求仅基于以下资料回答资料中没有的信息说不知道并且给出引用格式要求。失败模式二检索内容互相矛盾。多个文档对同一问题给出不同答案模型可能随机选一个或者编一个折中答案。这时候需要在检索层做去重和冲突检测把矛盾的内容标记出来让模型显式处理冲突。失败模式三检索不到相关内容但模型硬答。这是最危险的会产生幻觉。我的做法是设置一个相关性阈值检索结果的相似度低于阈值时直接告诉模型没有找到相关资料让它明确回复无法回答而不是硬编。def build_rag_prompt(query: str, retrieved_docs: list, threshold: float 0.7): relevant [d for d in retrieved_docs if d[score] threshold] if not relevant: return f用户问题{query}\n\n没有检索到相关资料请明确告知用户你无法基于现有资料回答。 context \n\n.join([f[资料{i1}] {d[content]} for i, d in enumerate(relevant)]) return f基于以下资料回答用户问题。如果资料中没有相关信息明确说明。 资料 {context} 用户问题{query} 4.3 向量库选型别一上来就上专用向量数据库很多团队一提到 RAG 就想着上 Pinecone、Milvus 这类专用向量库。我的建议是数据量在百万级以下先用 pgvector。原因很简单你已经有 Postgres 了不用引入新的运维负担事务、备份、权限这些都能复用。pgvector 的 HNSW 索引在百万级数据上性能完全够用。数据量上到千万级、或者 QPS 要求很高时再考虑专用向量库。这时候要评估的是索引构建速度、内存占用、分布式能力、过滤查询性能。Milvus 和 Qdrant 我都用过各有取舍但这不是一开始就要纠结的事。真正影响 RAG 效果的往往不是向量库选型而是分块策略、embedding 模型、rerank 模型这三件事。把精力花在这上面回报比换向量库大得多。5. Agent 编排让模型自己决定下一步的代价与收益5.1 Agent 的本质是一个循环不是一个魔法Agent 听起来很玄但拆开看就是一个循环模型观察当前状态决定调用哪个工具执行工具把结果反馈给模型模型再决定下一步直到任务完成或达到终止条件。这个循环的每一步都是 LLM 调用所以成本和延迟是线性叠加的。理解这一点很重要因为它决定了 Agent 的适用边界。简单任务不要用 Agent。如果任务步骤固定直接写死流程workflow比让模型自由发挥更可靠、更便宜、更快。Agent 的价值在于步骤不确定、需要根据中间结果动态调整的场景比如开放式研究、复杂问题排查。我的一般判断标准如果这个任务的步骤能用流程图完整画出来那就用 workflow如果画不出来或者分支多到画出来也没法维护才考虑 Agent。5.2 工具设计Agent 的能力上限由工具决定Agent 能做什么取决于你给它什么工具。工具设计有几个原则工具粒度要适中。太细模型要调用很多次才能完成一件事成本和延迟都高太粗模型无法灵活组合。我一般按一个完整的业务动作来设计工具比如查询订单状态是一个工具而不是连接数据库执行 SQL解析结果三个工具。工具描述要精确。模型靠描述来决定什么时候用哪个工具。描述里要写清楚这个工具做什么、什么情况下用、参数含义、返回值格式。我见过因为工具描述模糊导致模型反复调用错误工具的案例改描述后问题直接消失。工具要有幂等性和错误处理。模型可能重复调用同一个工具工具本身要能处理这种情况。工具执行失败时返回的错误信息要结构化让模型能理解失败原因并决定重试还是换方案。tools [ { name: search_documents, description: 在知识库中搜索相关文档。当需要查找事实性信息时使用。, parameters: { type: object, properties: { query: {type: string, description: 搜索关键词或问题}, top_k: {type: integer, description: 返回文档数量默认5} }, required: [query] } } ]5.3 终止条件和死循环防护Agent 最怕的就是死循环模型反复调用同一个工具或者在不同工具之间来回跳永远不结束。防护措施有几层最大步数限制。硬性限制 Agent 最多执行 N 步超过就强制终止并返回当前结果。N 根据任务复杂度设一般 10 到 20 步。重复检测。记录最近几步的工具调用如果发现连续重复相同的调用强制中断或者注入提示让模型换策略。成本预算。给每个 Agent 任务设一个 token 或金额预算超了就停。这个在生产环境特别重要防止某个异常任务把成本打爆。人工确认点。对于有副作用的操作比如发邮件、改数据在执行前插入人工确认或者至少记录审计日志。Agent 自主性越高这个越重要。注意Agent 的每一步决策都应该被完整记录包括输入、输出、工具调用、耗时、token 消耗。出问题时这些日志是唯一的排查依据。6. 成本、可观测性与评测AI Native 系统的运维底座6.1 成本必须从第一天就开始追踪LLM 调用是花钱的而且花得很快。如果不从第一天就追踪成本等你发现账单异常时已经晚了。追踪的粒度要到每次调用记录模型、输入 token 数、输出 token 数、单价、总价、关联的任务 ID 和用户 ID。有了这些数据你才能回答一些关键问题哪个功能最烧钱哪个用户的成本最高换个小模型能省多少缓存命中率是多少这些问题不追踪就永远不知道答案。我一般会在调用层做一个统一的 wrapper所有 LLM 调用都经过它自动记录成本。这样不用在每个业务代码里手动埋点也不会漏。6.2 可观测性日志、指标、追踪一个都不能少AI Native 系统的可观测性比传统系统更难因为多了模型这个黑盒。除了常规的请求日志、错误率、延迟指标还需要Prompt 和响应的完整记录注意脱敏用于调试和复现Token 消耗指标按模型、按功能、按用户维度检索质量指标召回率、相关性分数分布Agent 执行轨迹每一步的决策和结果缓存命中率包括精确缓存和语义缓存这些数据用 OpenTelemetry 这类标准框架采集接入现有的监控系统。关键是trace 要贯穿整个链路从用户请求到 Agent 决策到 LLM 调用到工具执行一个 trace ID 串起来。6.3 评测没有评测就没有迭代AI Native 系统最容易被忽略的是评测。传统系统有单元测试、集成测试但 LLM 的输出没法用断言来测。你需要一套评测集一批有标准答案或者有质量标注的输入输出对每次改动后跑一遍看效果是涨了还是跌了。评测集的构建是个持续过程。初期可以人工标注几百条覆盖主要场景。上线后把线上真实请求采样进来持续扩充。评测指标根据任务定分类任务看准确率生成任务看 BLEU、ROUGE 或者用 LLM as judge 打分检索任务看召回率和 MRR。LLM as judge 是个实用技巧用一个强模型给另一个模型的输出打分。虽然不完美但比人工快得多适合做回归测试。关键是 judge 的 prompt 要设计好给出明确的评分标准和示例。judge_prompt 你是一个严格的评审。请根据以下标准给回答打分1-5分 5分完全正确信息完整无幻觉 4分基本正确有小瑕疵 3分部分正确有明显遗漏 2分大部分错误 1分完全错误或答非所问 问题{question} 参考答案{reference} 待评回答{answer} 请只输出分数和简短理由。7. 从零搭建时的技术选型与落地顺序7.1 先跑通最小闭环再谈架构优化我见过太多团队在项目初期就纠结用哪个向量库、用哪个 Agent 框架、要不要上 Kubernetes结果两个月过去了还没跑通一个能用的 demo。正确的顺序是先用最简单的方式跑通端到端闭环哪怕是用 Flask 写个单文件、状态存内存、检索用暴力扫描只要能验证核心价值就行。跑通之后你会对真实的瓶颈有感知是检索不准是模型太慢是成本太高这时候再针对性地优化。过早优化是 AI Native 项目最大的时间杀手因为你对问题的假设大概率是错的。7.2 框架选型能用标准库就别用重框架Agent 框架这两年层出不穷LangChain、LlamaIndex、AutoGen 等等。我的建议是初期尽量少依赖框架。原因有几个框架抽象层多出问题难排查框架更新快升级可能破坏现有代码框架为了通用性做了很多你不需要的事增加复杂度和成本。我一般的做法是核心的 LLM 调用、状态管理、工具执行自己写代码量其实不大但完全可控。只在确实需要复杂编排比如多 Agent 协作时才引入框架而且要做好随时替换的准备。embedding 和向量检索同理pgvector 加一个简单的检索函数就能跑不需要一上来就上重型方案。7.3 团队能力建设AI Native 需要新的工程习惯最后说个容易被忽略的点AI Native 架构对团队的能力要求不一样了。传统后端工程师习惯确定性思维但 AI Native 要求你习惯概率思维——接受大部分时候对而不是必须对学会用评测和监控来管理质量而不是用断言。Prompt 工程也不是随便写写它需要版本管理、需要评测、需要 A/B 测试。我建议把 prompt 当成代码来管理存在版本控制里有变更记录有对应的评测结果。这样出问题时能快速定位是哪次 prompt 改动导致的。还有一点AI Native 系统的迭代节奏和传统系统不同。模型在更新prompt 在调整检索策略在优化这些都会影响最终效果。所以持续评测和灰度发布是必须的不能像传统系统那样攒一个大版本再上线。我在实际项目里最大的体会是AI Native 架构的难点不在技术本身而在于思维方式的转变。你得接受系统是活的——它会因为模型更新而变化会因为数据分布变化而退化会因为 prompt 微调而表现不同。架构设计的目标不是消除这种不确定性而是让不确定性可控、可观测、可迭代。把这一点想清楚了具体用什么工具、什么框架反而都是次要的。
返回列表