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

文章详情

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

AI工程化实战:从零搭建可靠AI问答助手的完整路径

AI工程化实战:从零搭建可靠AI问答助手的完整路径 去年年初我开始带一个小团队做AI落地项目接的第一个任务就是“从零搭一个能回答内部知识库问题的机器人”。当时的我天真地以为把大模型API接进来、扔几个文档索引进去就完事了。结果第一版demo跑起来后问题回答得驴唇不对马嘴上下文一长就失忆用户稍微换个问法就崩更别提什么幻觉和token成本的问题。踩了将近一个月的坑我才慢慢摸清“AI工程”和“调API”完全是两码事。这个“ai-engineering-from-scratch”的标题我觉得不能光把它当成一个接大模型接口的拼装教程而应该理解为一条从0到1把AI能力做成稳定、可控、可评估、可迭代的产品的完整路径。今天这篇文章我想把自己实际趟过的路、踩过的坑、最后沉淀下来的方案写出来主要面向那些已经会写代码、但对AI工程化还比较陌生的朋友。我不会去讲复杂的公式推导也不会吹某个框架多么万能我更想聊聊一个具体项目是如何一步步被拆解、设计、实现和纠偏的以及每一环背后的取舍理由。1. 先把“AI工程”这个词拆明白它不是调API很多刚接触AI开发的程序员看到“AI工程”第一反应是这不就是openai库调用一下然后写个UI把输入输出串起来吗这种认知在第一周确实够用因为跑通一个demo太容易了。但一旦要面对真实用户、真实数据、真实的业务约束问题就接踵而来。我用一句话来概括AI工程和传统研发的区别传统研发是“逻辑确定结果可预期”而AI工程是“逻辑不确定结果不可预期”。你无法预知大模型在某个输入上会产生什么输出所以整个研发范式必须从“写死逻辑”转向“设计一套约束、评估、兜底的系统”。这正是为什么近两年大家开始频繁提“prompt engineering”“harness engineering”“loop engineering”这些概念本质上都是在解决同一个问题——如何让一个概率模型变得可控、可预期、可用。从我自己的项目经验出发AI工程的核心可以拆成四层。第一层是模型选择。模型不是越大越好也不是越新越好而是要和任务复杂度、成本预算、延迟要求匹配。第二层是数据准备包括知识库清洗、分块、向量化这是RAG检索增强生成稳定输出的地基。第三层是系统设计核心是Prompt编排、上下文管理、工具调用Function Calling以及Agent的循环决策机制。第四层才是编码和部署把系统封装成服务配上监控、日志、评测反馈闭环。很多教程一上来就教你怎么用LangChain搭Agent、怎么接向量库这种“先跑起来再理解”的学习路径当然有价值但如果对上面的分层没有概念一旦碰到Bug就会很痛苦——你不知道问题出在模型本身、数据切片不对、Prompt写得有歧义、还是Agent循环设计有漏洞。所以我建议初学者在跑通demo之后一定要回头把每一层重新审视一遍否则后面所有调试都会像大海捞针。另外一个常被忽略的认知是AI工程本质上是“测试驱动”的工程。传统软件开发有明确的输入和期望输出可以写单测AI应用你也要写“测试”只不过这些测试是一个个评测用例集和评分标准。没有评测体系的AI项目基本等于蒙着眼睛开车你只知道“看起来还行”但不知道哪里会崩、怎么改进。2. 技术地基模型、上下文、提示词2.1 大模型选型的几个反直觉经验首先是模型的选型。很多人会有“用最强模型总不会错”的心态实际项目里这个想法会坑死人。我做过一个客服知识问答系统最初直接用当时最强的一线大模型回答质量确实好但延迟高、成本贵最离谱的是在夜间高峰期还会因为限流导致服务抖动。后来我把模型换成了同系列的中型版本配合优化的检索和提示词在90%的测试用例上效果没有明显差异但单位成本几乎降了一个数量级延迟也缩短了一半多。这里面其实遵循了一个原则模型能力要和任务难度匹配。如果任务只是“从给定的上下文里抽取答案”那么中小型模型完全够用如果任务是复杂的多步推理比如“先判断用户意图再查询多个数据源最后对比生成结论”才需要调用更大的模型。我现在的做法是双模型策略一个轻量模型负责意图识别和初步处理一个重量模型负责最终答案合成。这样既控制了成本又保住了复杂任务的质量。还有一点是关于模型接口的选择。我建议团队在项目初期就抽象出一层模型网关不要直接把某个厂商的SDK散落在业务代码里。因为大模型这个领域迭代太快今天你用的模型再好半年后可能就有替代品。我的团队一直在用LiteLLM这类框架统一封装不同厂商的接口改模型只改配置不动代码。这个决定在我们后面从OpenAI切换到国产模型时省了至少一周的改造时间。2.2 Prompt Engineering别把它当成写作文网上一搜Prompt Engineering满屏都是各种玄学技巧什么“角色扮演”“情感勒索”“思维链”之类的。我一开始也学得很起劲后来实践中发现那些花里胡哨的技巧在真实业务场景里的边际收益很小真正影响效果的是Prompt的结构和信息的组织方式。我自己通常把Prompt写成模板化的三段式系统角色与任务定义告诉模型你是谁、要做什么、输出格式是什么上下文输入区把检索结果、用户问题、业务规则组织成结构化的输入约束与兜底策略明确什么不能做、遇到未知答案时怎么回答比如我们做的内部知识问答系统系统提示词大概长这样你是一名企业IT支持助手。根据下面提供的参考资料回答用户的问题。 规则 1. 只能基于参考资料作答不得编造步骤或命令。 2. 如果参考资料无法回答问题请明确回答“资料中没有相关信息”并建议用户联系IT支持。 3. 回答采用Markdown格式步骤类问题用有序列表呈现。 4. 引用来源时在回答末尾标注[1][2]。 参考资料 {context} 用户问题 {question}这种写法的好处是每一轮对话的输入结构都非常稳定模型在结构化输入下通常会更稳定地输出。而且把规则、上下文、问题分开后后续做日志分析时也更容易定位问题出在哪一块。关于Prompt我还有一个特别想强调的点迭代Prompt的时候一定要保存版本。我自己有过惨痛经历——某天下午调Prompt效果变好了特别开心结果第二天发现某个边角案例又崩了却怎么都回想不起昨天改了什么。后来我所有的Prompt模板都会放进Git仓库一个模板就是一个文件命名带版本号。改Prompt和改代码一样要能提交、能回滚、能对比评审。2.3 上下文管理与应对“失忆”“AI聊着聊着就忘了之前说了什么”——这是新手上线AI应用后最常见的投诉。很多人的第一反应是“把这个模型换成上下文更长的版本”效果确实立竿见影但这会引发新的问题上下文越长token费用越高响应越慢而且某些模型会在长上下文中出现“注意力分散”反而把关键信息丢掉了。我实际摸索下来最管用的方式有三个一是设计意图压缩与摘要当多轮对话超过一定长度时把之前的对话交给模型生成一版结构化摘要之后只传递摘要而非完整对话二是关键信息提取把对话中出现的用户信息、关键实体比如“我叫李华用的Windows系统”单独提取出来持久化存储每次请求时重新注入而不是靠模型从历史里回忆三是多轮检索覆盖每轮提问都重新检索知识库而不只是依赖上一轮的检索结果。这里我想分享一个“上下文预算”的经验。我们做Agent开发时会在前端计算每次请求预计消耗的token数超出预算就会触发摘要压缩策略。这个预算值不是随便拍的而是根据模型上下文窗口和任务复杂度来算的。比如一个模型上下文是8K我们只用到6K留2K给系统提示词、工具定义和输出空间余量。这样就避免了一个常见事故某个步骤生成的内容太多导致后续步骤没有空间输出整个流程就断了。3. Harness Engineering 与 Loop EngineeringAI应用的骨架工程如果说Prompt是给模型写“台词”那么Harness Engineering就是给模型搭“舞台”。这个概念如果不理解AI应用就永远停留在“能响应”的阶段达不到“可靠交付”的水平。3.1 什么是Harness把模型包进一套可控的壳“Harness”直译是“马具、挽具”就是把马的力量约束到正确的牵引方向上。在AI工程里Harness指的是围绕大模型建设的一整套外围机制让模型输出变得可控。它至少包括这么几块。第一结构化输出控制。如果你直接让大模型“自由发挥”会得到一个什么都好但程序很难解析的文本。工程上最可靠的办法是强制模型输出JSON格式再用代码做schema校验。用OpenAI系模型的时候可以直接用JSON Mode或者Function Callingfrom openai import OpenAI client OpenAI() tools [ { type: function, function: { name: generate_answer, description: 根据上下文生成最终回答, parameters: { type: object, properties: { answer: {type: string, description: 最终回答内容}, confidence: {type: number, description: 置信度 0-1}, sources: {type: array, items: {type: string}} }, required: [answer, confidence, sources] } } } ] response client.chat.completions.create( modelgpt-4o-mini, messages[...], toolstools, tool_choice{type: function, function: {name: generate_answer}} )有了schema约束下游代码才能拿到可信的字段。我在生产环境里还会用Pydantic做二次校验字段解析失败就触发重试而不是直接把原始文本扔给用户。第二工具调用与行动边界。真实业务中模型需要“动手”做事比如查数据库、调内部接口、发邮件。Harness工程在这里规定了哪些工具可以被调用、参数怎么传入、调用结果的解析方式。更重要的是权限隔离——模型是一个概率系统它可能在任何时候发起一个你不想让它发起的调用所以必须在Harness层面对工具权限做白名单限制。第三数据流追踪与可观测性。“黑盒AI”是工程实践中最大的恐惧。当用户反馈某个问题回答错了时你必须在几分钟内定位是模型问题、检索问题还是Prompt问题。我给每个请求分配一个Trace ID串联起输入Prompt、检索到的文档、模型中间输出、最终输出、耗时和token数。后面用的是Langfuse作为追踪平台它能可视化地展示每一步发生了什么。没有这套数据追踪的AI项目出了问题就像警察破一件没有监控的案子全靠猜。3.2 Loop Engineering让AI在循环里自我纠错Loop Engineering这个热词去年开始火核心意思是不要指望大模型一次推理就能得到完美结果而是设计一个循环——执行、检查、反馈、修正直到达到停止条件。这个思想和软件开发里的“重试机制”很像但复杂在“检查”这一步。我做得最多的一个Loop场景是“生成SQL查询并执行”。第一版Agent直接让模型生成SQL然后直接执行结果翻车率极高——表名写错、字段名幻觉、语法不兼容五花八门。后来我设计了一个三阶段循环第一阶段模型生成SQL第二阶段模型自检SQL对照数据库schema检查表名、字段名、聚合逻辑第三阶段代码层先以EXPLAIN方式执行SQL确认语法合法后再真正执行如果结果异常把错误信息喂回给模型要求它修正。简单算一笔账一套SQL查询流程平均需要2.1次模型调用也就是大约40%的请求会进入至少一轮修正循环。看起来调用次数变多了、成本变高了但最终交付的正确率从第一版的62%提升到了94%。这个性价比非常值得。Loop设计里有几个关键参数。一个是最大循环次数max_iterations。我一般设3到5次超过就直接放弃转为人工处理或输出兜底话术。为什么不能无限循环因为模型可能会在同一条错误上来回打转无限循环只会无限烧钱。另一个是“反思提示词”的设计。自检指令不能太笼统要具体到关键检查项。比如SQL场景的自检提示词我会写成请检查以下SQL是否可以在给定的数据库schema下正确执行 1. 所有表名是否在schema中存在 2. 所有字段名是否存在于对应表中 3. JOIN条件是否有潜在笛卡尔积风险 4. WHERE条件是否与字段类型匹配 5. 如果SQL有语法错误请指出具体位置并给出修正版本。这样的Loop才能精准纠错而不是让模型在那里泛泛地“请检查一下”。3.3 Agent与多Agent协作从单模型到多角色流水线当任务复杂度再上一个台阶比如“用户问了一个问题需要先判断意图、再查多个系统、中间可能还要做计算”就需要引入Agent和Agent协作机制。我非常推崇一种“流水线式多Agent”模式而不是“让多个Agent自由讨论”。自由讨论在技术演示上看着科幻但工程上很难控制谁在主导、怎么收敛、怎么分配权重都是问题。我的做法是把复杂任务拆成有序的流水线角色Planner规划者把用户需求拆解成子任务序列Researcher研究员负责检索资料、调用外部数据Writer撰写者基于前序结果生成最终回答Reviewer审查者检查回答质量、事实一致性、格式合规。每个角色都会设定独立的Instruction和输出schema下游拿到上游的结构化结果后继续处理。我这边实测的感受是多Agent流水线比单一Agent多轮反思的效果更稳定因为每个环节职责单薄Prompt不容易失焦测试也更好做。当然这个模式会增加延迟和成本。我的实践是用这一套来处理那些“高价值复杂任务”而简单问题走轻量单模型链路在入口处做个路由分流。这个设计其实和技术架构里的“读写分离”思路异曲同工——不同负载走不同通道各取所需。4. 从零实操两周搭出一个可上线的AI问答助手前面讲了很多理论和框架这一节我想回到最关键的落地环节如果现在给你一个全新的项目怎么从零开始、在一个可接受的周期内交付一个能扛住真实用户流量的AI问答助手。我会按我自己实际走过的流程来讲包含技术选型的理由、关键代码结构、配置参数的计算过程。4.1 第一周需求定义和方案选型在写第一行代码之前我会先做一个“最小可用范围”的界定。不要一上来就想把所有知识库文档灌进去而是先圈定一块高频且边界清晰的语料。拿内部IT支持场景来说初期只做“常见软件安装与账号故障处理”这一小部分而不是全公司所有业务系统的知识问答。范围小数据清洗难度低评测也好建立。技术选型方面我推荐新手从一套“轻量自研组合”起步而不是直接上LangChain全家桶。原因很简单LangChain的抽象层次太高出了问题你根本不知道底层发生了什么而自研方案的代码量其实没有想象中那么大核心流程用FastAPI加一百多行代码就能写出来。这能帮助你真正理解每一步在干什么。我们当时的技术栈是FastAPI做服务层、SQLite存会话状态、向量检索用ChromaDB本地起步后续可以平滑切到Qdrant或Milvus、模型层用LiteLLM统一封装、嵌入模型用开源的BGE-M3中文效果好、成本为零、评测和追踪先用Langfuse Cloud免费额度。4.2 核心实现RAG和结构化输出从数据到可检索的知识库需要做三步清洗、切分、向量化。切分chunk大小是第一个需要调的参数。我踩过不少chunk_size的坑太小了检索到的片段信息不完整太大了又会混入无关噪声、浪费向量库存储。以中文技术文档为例我最终选择了chunk_size400字符、overlap80字符的组合。这个参数的逻辑是中文一个汉字大致对应1到1.5个token400字大约500-600个token既不会太短导致语义断裂也不会太长导致检索精确度下降。向量检索的核心参数是top_k也就是返回几篇最相关的文档给模型。top_k设少了可能漏掉关键信息设多了无关信息会干扰模型生成。我自己习惯先设5然后根据评测结果微调。另外检索后我还会做一个重排rerank步骤用一个轻量级的交叉编码器模型给候选结果重新打分可以把准确率再往上提一截。重排对检索质量的价值非常大但因为它增加了整体延迟通常只在“首次检索后、送入模型前”执行一次。结构化的核心部分我参考了“Query Rewrite Function Calling 结果校验”的模式。用户提问后先由模型做意图重写和检索关键词提取然后进入工具调用流程最后由校验层做输出字段完整性检查不合法就走Loop重试。整个链路的伪代码大概是这样from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QARequest(BaseModel): question: str session_id: str class QAResponse(BaseModel): answer: str sources: list[str] confidence: float app.post(/api/qa, response_modelQAResponse) async def qa_endpoint(req: QARequest): # 1. 检索向量库拿到候选文档 docs retrieve_top_k(req.question, top_k5) # 2. 用大纲Prompt构建系统消息 system_prompt build_prompt_template(docsdocs, questionreq.question) # 3. 调用模型启用工具模式获得JSON result call_model_with_tools(system_prompt) # 4. 解析并校验输出不合法则触发一次Loop parsed parse_and_validate(result) # 5. 写入追踪日志 log_trace(req.question, docs, result, parsed) return parsed4.3 评测集和生产调试AI测试开发的本质如果没有评测集优化就无从谈起。我做这套系统时最先投入时间的不是写代码而是标了一百二十条典型问答对。每条包含用户问题、期望回答中的关键点、检索期望命中的文档ID。有了这个评测集每次改Prompt或检索策略时我都能批量跑分看涨跌。评测跑分我用的是一个大模型作为评委LLM-as-a-judge从三个维度打分相关性和完整性用1-5分打分以及“是否忠实于资料”有没有幻觉。单靠模型当裁判会有偏差所以每个维度还要抽样人工复核。当模型裁判和人工判断出现分歧的时候优先以人工为准并把分歧样本加入未来评测集持续补充覆盖度。如果你要接手一个已有AI系统第一件事也是建评测集。别相信当前“效果还行”的感觉只有把它量化后续才有优化空间。这其实和“测试开发”的思路完全一致没有自动化测试的代码库重构就是自杀没有评测集和回归测试的AI项目优化就是在走钢丝。4.4 上线部署与成本从demo到生产的距离部署方面我把应用打包成Docker镜像用云上的GPU/CPU实例跑服务。隐藏的坑是Embedding模型的运行需要内存输入请求频繁的时候CPU会飙升。我们在生产环境给“重排”单独开了一个小实例避免和主服务抢资源。切分不同尺寸的模型还有一个成本公式。假设每天一万次请求每次请求消耗约两千token输入和五百token输出。用一款轻量版模型的成本大约是每百万token几块钱一天的成本也就是几块钱如果全量换成更大更强的模型单位成本涨了几倍甚至一个数量级。这个差距乘以三十天就是真金白银。做技术方案的时候不算清楚这笔账交付给运维和财务的时候会被打得很惨。上线后的监控维度除了常规的延迟、错误率我强烈建议监控三个AI专属指标召回准确性检索到的文档到底相不相关、忠实度回答内容是否都能在资料里找到依据、落库率结构化输出被校验层一次性通过的比例。这三个指标能直接反映整个AI链路的健康状况。5. 常见问题与排查技巧实录真到了实战环境问题永远是五花八门的。这里记录几个Type里最高频的问题和排查思路都是我实际遇到过的。5.1 幻觉问题模型一本正经地胡说八道幻觉是AI应用最大的原罪。我排查幻觉通常从两个方向入手一是看模型是否无视了Prompt里的“只能基于上下文回答”的指令二是看检索结果本身是否相关。如果检索结果就是错的模型再忠实也白搭。治本之策是把“无法回答”变成一个合法出口。我在Prompt里专门加了一段如果资料中不存在问题的答案请直接回复“当前资料库中未找到相关信息”不要试图猜测或编造。并且在评测集里专门构造了一批“无答案样本”让模型学会判负。有人担心这样会让用户体验变差但事实相反——一本正经的错误远比“我不知道”伤人。5.2 上下文越长效果越差失忆与注意力分散上文提过上下文预算这里补充一个小技巧。我会在Send给模型的Messages里把“最近一轮对话”放在靠前位置把历史摘要放在中间系统提示词放最前。这个排布是参考了Transformer注意力机制的原理——离输出位置越近的token注意力权重通常越高。实测调整后多轮对话的连续性有明显改善。同时对话系统必须做“会话重置”机制。在会话空闲超过一定时间后清空上下文改用用户画像和历史摘要重新引导。这件事不做长尾对话的token消耗会不断膨胀最后不可避免地撞上上下文窗口上限。5.3 评测难打分不稳定人工太贵这是最折磨人的问题。用大模型当裁判换一个Judge模型版本分数全变用人工测评一天测不了几十条。我现在的做法是“分层评测”第一层用轻量规则做硬校验比如有没有输出JSON、字段是否齐全、是否引用来源第二层用大模型评分官做语义评分第三层定期抽少量样本人工评估校准。整个评测流水线串起来后跑一次全部评测只需二十分钟可以支持一天多次迭代。5.4 集成测试飘忽不定时好时坏如果同一个输入这次正确下次错误先别急着怀疑代码有随机Bug大概率是模型本身的采样温度太高。检查一下Configuration里的temperature如果偏高把它降到0到0.2之间。大多数业务问答场景都不需要模型“发挥创意”采样温度越低确定性越强。反过来如果做头脑风暴类产品才需要提高temperature。我还习惯把“top_p”也稍微调低双管齐下稳定输出。顺便附一个排查用的速查表方便你以后遇到问题直接对照。现象可能原因优先排查方向回答偶尔充满幻觉检索质量差 / 上下文无关检查文档切分和重排逻辑多为JSON解析异常工具调用未生效检查tools定义及参数schema对话轮换后信息丢失历史摘要被截断检查上下文预算和摘要策略输出延迟过高模型过大 / 重排耗时换轻量模型、优化重排通道费用暴涨循环无上限 / 上下文持续膨胀设置迭代与预算开关同样问题结果飘忽temperature过高调整为0-0.2根据我个人的经验做AI工程最重要的不是把最新最酷的模型接进来而是在模型外面把“围栏”立起来。每一个看似普通的Harness组件——退避重试、schema校验、评测回归、trace追踪——单独拿出来都不起眼但组合在一起决定了你的AI产品是“偶尔惊艳还是经常稳定”。如果你也在从零搭自己的AI项目在这个阶段我最有诚意的建议是尽早把评测集和可观测性做起来哪怕前期只有几十条用例。后续不管是换模型、调Prompt、加Agent还是做Loop你都能量化地知道改动带给你的是进步还是退步。这两样东西就是AI工程中真正的复利资产。
返回列表