需求来了一周内上线一个内部知识库问答机器人。我当时的第一个念头是——打开某个低代码 AI 平台拖几个节点把流程串起来半天做一个 Demo。结果 Demo 确实能跑领导也点头但真要接到生产环境的时候麻烦全冒出来了。提示词改不动、分支逻辑绕来绕去、日志不完整最致命的是我根本没法用 Git 回溯每一步配置。就是从那一刻起我把“代码可控”当成了选型的第一原则也才有了后来这套 BuildingAI 的玩法。BuildingAI 不是什么神秘框架我给自己的 AI 应用脚手架起的代号而已核心就四个字灵活、代码可控。它不排斥低代码平台的便利但把所有关键路径都变成显式的代码、可测试的函数、可追溯的配置。这篇文章我会从需求拆解、核心设计、实操案例到问题排查把我自己踩过的坑和沉淀下来的工程模式完整讲一遍。无论你是 AI 工程师、后端开发还是想认真搭一个 Agent 服务的产品经理应该都能从中找到能直接抄作业的部分。1. 为什么我会把“代码可控”放到 AI 构建的首位1.1 低代码平台踩过的坑最早做售前资料问答机器人时我用了一个可视化 AI 平台过程确实顺滑左侧拖一个“知识库检索”节点右侧拖一个“大模型生成”节点中间用连线串起来再填一个提示词模板十分钟就能看到一个能回答问题的界面。可事情一到“改”就变味了。业务同事说“当用户问的问题跟资料无关时能不能先回复一句‘这个问题我暂时无法回答’而不是硬编”我打开平台找到判断节点发现这个节点只能填单条规则多个条件就要嵌套。嵌套了两层之后页面变成了蜘蛛网节点上的线交叉在一起别说别人我自己都看不懂。更要命的是平台没有 diff没有评审我每点一次保存线上配置就变了而且没有任何记录。有一次我不小心把“知识库相似度阈值”从 0.6 改成了 0.2整个问答质量断崖式下降我花了两个小时才定位到原因。那次以后我认真复盘问题不是出在“低代码”这个形式而是出在“不可控”。配置可视化本身没有错可它把所有逻辑压进了一张隐式图里没有入口参数没有返回值没有单元测试更谈不上版本管理。核心路径一旦复杂整个系统就像用橡皮泥捏的房子看着能立住一碰就塌。1.2 “灵活”到底指的是什么很多人在选型时把“灵活”等同于“参数多、配置项多、能拖拽的东西多”。我不这么看。真正的灵活是在不推翻整体结构的前提下任意替换某一个环节的能力。我把它拆成三个层面结构灵活流程可以是顺序执行也可以是条件分支、循环、并行甚至动态生成子流程而不是只能靠死板的连线。数据灵活每一步产出的中间结果可以被任何下游步骤访问可以转换格式、补充字段、丢弃无用数据而不是把数据结构锁死在平台里。部署灵活同一套逻辑既可以跑在本地脚本里做实验也可以打包成一个 HTTP 服务还能放进 CI/CD 流水线里做回归测试。代码可控则是保证这种灵活不失控的关键。代码意味着可读、可改、可测试、可版本化。一段流程定义放在 Git 里每次改动都能看到 diff一个步骤函数可以单独用单元测试跑一遍一条异常日志可以直接定位到具体代码行。这些能力在图形化配置里几乎很难做到。用表格感受一下差异更直观维度传统可视化 AI 平台BuildingAI 代码方式流程修改拖动连线嵌套复杂修改代码diff 清晰调试平台日志颗粒度粗单步断点任意打印测试基本没有单元测试 集成测试版本管理无或很弱Git 全量管理复用复制项目导入函数/模块上线点发布按钮标准部署流程当然这不是说所有场景都必须代码化。如果只是临时搭个 Demo 验证想法可视化平台依然很快。但一旦你要做的是“会上线、会被多人改、会长期维护”的 AI 应用代码可控就是生存问题。2. 认识 BuildingAI一个以代码为中心的 AI 编排层2.1 它解决的核心矛盾AI 应用和传统后端最大的区别在于模型输出具有不确定性但工程系统又追求确定性和可维护性。两者天然有张力。举个例子。你调用大模型让它在“需要追问”和“直接回答”之间选一个模型可能今天选 A明天选 B甚至同一批请求里结果都不一样。如果你把这种随机性扩散到整个应用——一会儿走这个分支一会儿走那个分支日志也乱七八糟——那你就很难定位问题到底是模型抽风还是检索没召回到底是提示词写错了还是上游数据传错了BuildingAI 的思路很简单把不确定性关进笼子里。流程骨架用代码固定下来每一步是一个明确的函数模型只负责“生成候选文本”或“做某个局部决策”决策结果还要经过规则校验不合规就重试或走兜底分支。这样随机性被局限在一个个小格子里外面依然是稳定、可测试的工程结构。打个比方这就像做菜。低代码平台给你一个自动炒菜机按几个按钮就行但你不能随便换锅换火候BuildingAI 则是给你一套完整的厨具和菜谱每道工序都能自己控制。你当然要多花一点心思但至少不会因为炒菜机的一个默认参数毁掉整锅菜。2.2 核心模块拆解我把一套可复用的 AI 应用脚手架拆成了下面几个部分实际落地时可以根据项目增删Flow流程定义层负责描述“先做什么、再做什么、满足什么条件走哪条路”。它是一张有向图节点就是普通函数边就是调用关系。Flow 本身不关心业务细节只负责调度。Tools工具接入层把外部能力统一封装成函数比如向量检索、查数据库、调用内部 API、读写文件。每个 Tool 有明确的输入输出 schema方便校验和复用。Model模型适配层统一封装不同模型提供方的接口给上层提供相同的调用方式。切换模型时不用改业务代码只要换一个 adapter。Memory记忆管理层管理对话历史、短期缓存、长期用户画像控制哪些信息进提示词哪些信息只做参考。Guard校验与兜底层对模型输出做格式校验、内容过滤、引用检查失败时触发重试或降级策略。Trace追踪与日志层记录每一步的输入输出、耗时、模型参数、工具调用结果方便事后排查。这六个模块不是必须全部具备但 Flow、Tools、Model、Trace 这四个我建议无论如何都保留。理由很简单没有 Flow流程会散落在业务代码里没有 Tools外部能力会和各种逻辑搅在一起没有 Model换模型等于重写没有 Trace出了问题只能靠猜。3. 用 BuildingAI 搭一个可上线的 RAG 智能问答服务3.1 场景需求与选型为了讲清楚怎么落地我用一个最常见的场景做例子企业内部制度问答机器人。需求有三个用户提问后机器人先判断能不能直接回答如果问题缺少关键信息比如只问“年假怎么休”但不说是正式员工还是实习生先追问澄清。回答必须基于知识库检索到的资料不能凭空编造。回答后面要附带引用的文档标题方便用户核对。这种需求看似简单但如果你直接把“检索 拼接上下文 让模型生成”三段式一写很快就会遇到问题模型会把所有检索到的内容都当作有效内容哪怕其中有几条跟用户问题无关它也能编出一段“看似合理”的答案。所以必须加前置判断、后置校验这些逻辑用代码控制最顺手。3.2 编写流程定义我先给出一个 BuildingAI 风格的流程定义示例。下面这段代码不是某商业平台的配置而是我自己的工程模式你可以把它当作伪代码来理解核心是“每个步骤都是一个函数流程是一段可阅读、可测试的编排逻辑”。from buildingai import Flow, step, Tool, LLM Flow def enterprise_qa(): # 第一步接收用户输入 query step(input) # 第二步读取当前会话历史 history step(memory.load, keyquery.session_id) # 第三步判断意图是否需要澄清 intent step( llm.check_intent, queryquery.text, historyhistory, schema{ type: object, properties: { need_clarify: {type: boolean}, clarify_question: {type: string} } } ) if intent.need_clarify: # 第四步直接回复追问 return step(output, intent.clarify_question) # 第五步检索知识库 docs step( retriever.search, queryquery.text, top_k4, min_score0.3 ) # 第六步生成答案 answer step( llm.generate_answer, queryquery.text, docsdocs, historyhistory ) # 第七步校验引用是否存在 cited step(guard.check_citation, answeranswer, docsdocs) # 第八步返回结果 return step(output, cited)这段代码看起来很简单但好处非常大整个请求的生命周期一目了然中间任意一步出问题都能在Trace里看到。你甚至可以单独测试retriever.search这个函数输入一个 query看它返回的 docs 是否符合预期。3.3 把工具和 API 接进来的细节RAG 的核心是知识库检索。这里我常被问到是不是一定要用向量数据库我的答案是看你的数据量。如果知识库只有几百篇文档直接用基于关键词的 BM25 检索都能打如果到了几十万篇再上向量召回也不迟。在我的项目里我会把检索逻辑封装成一个 Tool统一接口from buildingai import Tool Tool def retriever_search(query: str, top_k: int 4, min_score: float 0.3): # 实际项目里可以替换为向量库、ES、SQL LIKE任意检索实现 results vector_store.search(queryquery, top_ktop_k) filtered [d for d in results if d.score min_score] return [{title: d.title, content: d.content, score: d.score} for d in filtered]封装的好处是上层流程不关心检索到底走的是向量库还是倒排索引。今天用本地faiss明天换成线上的向量数据库只需要改这一个函数其他代码不用动。同理对接内部系统、读取订单状态、查询员工信息都可以按照这个模式封装。有一个细节要注意Tool 的输入输出一定要做类型定义。Python 这种动态语言虽然方便但在流程复杂以后很容易出现“这个步骤传给我的是字符串我以为是对象”的问题。我习惯在每个 Tool 上加 Pydantic 模型做校验出错时立刻报出来而不是等到生成答案才发现数据不对。3.4 加入记忆与上下文控制多轮对话里记忆管理是最容易翻车的地方。一个常见错误把整个对话历史全部塞进提示词既不截断也不压缩。等聊了二十轮以后上下文可能超过模型窗口或者早期对话内容把模型的注意力带偏导致当前回答质量下降。我的经验是分级处理短期记忆只保留最近三轮对话作为当前问答的直接上下文。长期记忆将每一轮对话总结成一句话存进场景摘要例如“用户是实习生关注年假政策”后续对话中一旦涉及相关主题就把摘要加入提示词。会话元信息用户 ID、部门、角色用于过滤知识库权限。在 BuildingAI 的memory.load步骤里我会返回一个结构包含上面三个层级{ recent_messages: [...], # 最近3轮 summary: 用户是实习生关注年假政策, metadata: {role: intern, dept: sales} }然后在生成答案时根据意图决定哪些内容要进入提示词。这样既控制 token 消耗也避免无关历史干扰模型。3.5 运行与联调当流程和工具都封装好以后启动服务就很简单了。我通常会把流程暴露成一个 FastAPI 接口内部调enterprise_qa并注入请求参数。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QARequest(BaseModel): session_id: str text: str app.post(/qa) def ask(req: QARequest): result enterprise_qa( queryQAQuery(session_idreq.session_id, textreq.text) ) return {reply: result.reply, citations: result.citations}这样做的目的是把 AI 流程和后端框架解耦。流程本身不依赖 FastAPI你可以在 Jupyter 里直接调它做实验也可以把它挂到别的 Web 框架上甚至写成一个 CLI 工具在本地批量测试。联调阶段我会重点看 Trace 日志尤其是每一步的耗时和输入输出大小{ step: retriever.search, input: {query: 实习生年假几天, top_k: 4}, output: [ {title: 考勤管理制度, score: 0.82}, {title: 实习生管理办法, score: 0.71} ], duration_ms: 128 }拿到这类日志你才能回答最核心的问题这个应用的时间到底花在哪了是模型生成太慢还是检索太慢是上下文太长导致 token 消耗过大还是工具调用阻塞了流程没有 Trace这些问题只能靠猜。4. 实战中的常见问题与排查技巧4.1 模型输出不稳定现象同一个问题上午回答正常下午开始胡说或者同样一组输入模型一会返回 JSON一会返回纯文本。原因往往有三个提示词写得太宽松、模型温度参数偏高、没有对输出做结构化约束。我现在的基本做法是所有“决策类”输出一律要求模型返回结构化 JSON并且用 schema 校验。比如判断是否需要澄清不要让模型自由发挥而是要求它输出一个布尔值加一个追问文本请根据用户问题判断是否需要澄清。 输出格式为 JSON {need_clarify: true, clarify_question: ...}然后在代码里用 Pydantic 做强校验解析失败就自动重试一次。重试时可以把上次的错误信息附带给模型让模型自我修正。温度参数方面决策类任务我会调到 0.10.3生成类任务也尽量不超过 0.5避免发散。4.2 Agent 陷入死循环有些 AI 应用在引入 Agent 工具调用后会出现一个经典问题模型不停地调用工具却不返回最终结果。比如它先搜了 A又搜了 B觉得还不够又搜了 A循环往复最终把 token 烧光。解决办法非常朴素在 Flow 执行器里加最大值门槛。我一般会设三层限制最大工具调用次数例如 5 次超过后强制停止。最大执行时间例如 30 秒超过后立即返回当前结果。最大 token 消耗例如单次请求 8000 token接近阈值就触发压缩或终止。这三层限制看起来简单但在生产环境能救命。有一次我在测试 Agent 时模型连续调了七次工具每一次都把上一轮结果重复拼进上下文最后回答的质量并没有变好纯属浪费。加限制后模型被迫在更少的步骤内做决策反而更稳定。4.3 外部 API 超时与重试RAG 服务依赖的组件越多超时风险越高。向量库可能慢模型 API 可能网络抖动内部系统可能过载。如果什么都不处理用户只会感觉到“转圈圈”。我在封装 Tool 时每一层都定了明确的超时时间检索工具 2 秒模型调用 15 秒内部接口 5 秒。超时后不盲目重试而是根据错误类型决定连接超时退避重试最多 2 次。服务器返回 5xx指数退避最多 3 次。参数错误 4xx不重试直接记录并走兜底逻辑。另外有一个容易被忽略的点调用模型 API 时不要把整个请求体塞进日志。有一次我把完整上下文打进了日志里面包含用户的个人信息差点造成数据安全问题。现在所有日志都会先做脱敏只记录长度、耗时和关键状态不记录正文。4.4 提示词污染与记忆混乱多轮会话里模型会把历史对话中的“错误信息”当成事实。比如用户第一轮问“我今年休了 15 天年假”后一轮说“我今年只休了 5 天”模型可能不会纠正而是顺着上一轮的描述继续生成。这就是提示词污染。我的处理方案是给每轮对话加“事实时间戳”在记忆模块里标注哪些是用户陈述哪些是系统结论哪些是模型推测。用户在后续提问时如果新的信息与历史记忆发生冲突系统会优先采用当前轮次的用户陈述并把冲突标记出来。这样至少能避免模型把前后矛盾的内容揉在一起。另外一个低级但常见的错误把整个 system prompt 塞进每一条消息。memory.load返回的 summary 里如果带了过长的上下文会挤压真正当前问题的注意力。我会在进入生成步骤前对记忆内容做一次精简只保留当前意图最相关的事实其余内容放到“参考资料”字段中而不是直接拼进对话历史。问题现象排查思路解决方案模型输出不稳定同一问题结果差异大查看 Trace 中模型输入和参数结构化输出 schema 校验 低温度Agent 死循环工具调用次数过多检查 Trace 中步骤序列设置最大步数/时间/token 限制外部 API 超时用户等待时间长分组统计各 Tool 耗时分层超时 退避重试记忆污染回答受早期错误历史影响查看 memory.load 输出分级记忆 冲突标记5. 最后几句真心话如果你看完前面的内容决定立刻开始着手搭建自己的 AI 应用我有一句建议不要一开始就追求复杂框架。哪怕你只是用普通 Python 写一个函数把“用户输入 → 检索 → 模型生成 → 输出”的每一步拆开并且给每一步加日志和断言你已经在做 BuildingAI 的核心事了。代码可控不是套一个漂亮框架而是让关键路径有迹可循、可测试、可回溯。我个人在实际操作中还有一个体会把大模型当成一个普通函数来调会降低很多心理负担。它输出的内容不稳定没关系你在函数外面做校验、做重试、做兜底就和调用任何一个可能失败的第三方服务没有区别。先用“Trace 断言”把关键路径测起来再接大模型后面改动就不会痛不欲生。这套思路我从售前问答机器人用到现在已经帮我省下了无数个排查问题的夜晚希望你也能用得上。