
过去一年多我绝大部分时间都在做同一件事把大模型从demo推到生产环境。“AI工程化”这四个字听起来像个抽象概念真正做起来全是一个个具体的坑文档怎么清洗、检索为什么漏召回、模型回答如何溯源、接口超时该怎么办。这篇文章就是把这些坑和相应的解法按我自己的经验重新梳理一遍。我会用一个非常典型的项目作为贯穿全文的案例企业内部制度文档问答助手。项目规模不大几百份Word和PDF制度文档几十个员工高频问题但它几乎覆盖了AI工程化从零到一的所有关键环节——数据准备、检索增强、评测、部署、排错。如果你已经会调用大模型API跑过一些RAG demo但还没有把一个AI项目真正交付给用户日常使用这篇内容应该能帮你省下大量试错时间。1. 先梳理清楚AI工程化到底在“化”什么1.1 会调API不等于会做AI工程很多人第一次接触大模型应用时都觉得这就是“把问题丢给模型拿到回答包一层壳”没什么难的。我刚开始也这么想结果被现实教育得很惨。纯调API的demo用户问什么模型都答哪怕知识库里根本没有答案它也能一本正经地编。业务方看到这种demo通常会很兴奋等上线以后才发现制度更新了它还拿旧版制度回答文档里明明有答案它偏偏答不到点上最麻烦的是它永远意识不到自己正在犯错。到这一步技术负责人往往才意识到问题不是“换一个更强的模型”能解决的而是整条数据链路和系统设计都需要重新考虑。我后来总结过一句话AI工程化和传统软件工程最大的不同在于系统里多了一个概率性组件。传统程序是“输入A输出必定是B”AI程序是“输入A大概率输出B偶尔输出C小概率输出D”。工程化要做的事情不是追求某个模型把B的概率变成100%而是把C和D出现的概率压到业务可接受的范围同时当C和D真的发生时系统还有能力体面地降级、兜底、被人追查。1.2 30%模型、70%工程AI项目的真实成本结构我做过好几个AI落地项目之后得出一个可能让很多人意外的经验比例模型部分通常只占整个项目不到30%的工作量剩下的70%全部是工程活。这个70%具体花在哪以知识库问答为例大致是数据准备清洗、解析、切块、入库大约占30%检索与生成链路查询改写、召回、重排、引用校验大约占20%评测体系回归集构建、打分、badcase分析大约占20%服务化与监控接口、降级、日志、成本看板大约占15%模型选型与调参其实只占剩下的15%。这个比例当然会有波动项目不同侧重也不同。但我见过太多团队精力全放在“模型”两个字上换模型、试新prompt、调temperature折腾了几个月最后发现检索召回一塌糊涂换什么模型都白搭。模型是系统的上限数据、检索、评测这些工程环节决定的是下限而下限才是用户每天都在感知的东西。1.3 一个贯穿全文的具体场景锚点为了让后面所有技术选型和实操都落在一个真实语境里我在这里固定一个项目背景某企业要做内部制度问答助手。原始材料包括几十份Word制度、一部分扫描版PDF、一批历史FAQ。员工问的问题不会按标准题库出牌真实情况往往是“休产假要提前多少天申请”“差旅报销单的发票命名规则是什么”“请假单编号怎么查进度”。这些问题的答案散落在不同文档里而且制度每隔半年还更新一次。这个项目的MVP定得很明确支持文档上传与解析、支持检索增强回答、回答必须标注出处、管理员更新文档后系统能同步生效。同时我们也把不做什么想清楚了不做权限细分、不做模型微调。为什么把场景写到这么细因为后面每一个选型判断都依赖这些具体的约束。比如不设权限细分意味着检索可以全局共享制度半年更新意味着入库链路必须支持增量重建扫描件比例不低说明OCR是前置必修课。2. 技术栈选型哪些自己搭哪些站在巨人肩上选型的本质是权衡不是追新。我的判断标准始终只有三条第一数据能不能出内网第二团队能不能长期维护这套东西第三这个项目三到六个月后大概率还是会持续演进。围绕这三条我把每一层的选型思路讲透。2.1 模型层内部数据能不能出网决定第一刀往哪砍模型层的任何讨论都要先过数据合规这一关。在那个企业场景里客户IT部门一票否决了出网方案所以直接走了私有化路线。如果数据可以出网我的建议是先别急着上GPU先用商用API把整条链路跑通。私有化部署的算力申请、运维、模型迭代前期会吃掉大量本该花在业务逻辑上的工程时间。商用API和私有化开源模型的取舍可以简单看这张表维度商用API私有化开源模型数据出网需要数据出内网完全内网初始成本按量付费起步低GPU投入几万到几十万不等维护成本低高部署、监控、更新都靠自己效果天花板通常较高看参数量和调优水平适合阶段快速验证、数据不敏感数据敏感、规模稳定私有化部署不是一个“装个开源模型就完事”的动作。我们当时对模型可用性的评估不用跑分而是用自己的50条业务题把检索到的制度片段和问题一起丢给模型人工判断回答是否符合制度口径。超过85%的题目能被判定可用就继续推进达不到就换更大的模型或从prompt层面找问题。这个测试集后来演变成了正式的回归集一箭双雕。2.2 框架层先裸写再上框架别让抽象层绑架你关于LangChain、LlamaIndex这类框架我特别想多说一句因为这是新人最容易纠结的地方。框架解决的其实是长尾问题多工具调用、Agent记忆、连接器生态。而知识库问答的主干就是四步加载文档、切块、向量化、调模型生成。框架在这个主干上提供的便利很有限带来的麻烦倒不少——调试时要翻框架源码中间数据结构被框架封装得严严实实出问题时你绕不开它。所以我的建议非常明确第一版不依赖任何框架直接用普通Python代码把链路写通。先裸写的好处是你对每一步的数据格式都了如指掌出了问题就是自己的代码不存在黑盒。等链路稳定了如果场景里真的出现复杂的工具调用、多轮记忆、大量外部连接器再按需引入框架而且尽量选社区活跃、长期维护的。2.3 存储与检索层向量库不是越重越好向量库的选型是另一个典型的“过度设计”重灾区。不少团队一上来就上分布式向量数据库结果数据集连八万条向量都不到维护成本反而成了最大的负担。给一个粗略的选型参考方案定位适合规模运维成本Chroma轻量、嵌入式万级以下向量极低pgvector基于PostgreSQL扩展百万级向量中复用现有PGMilvus专业向量数据库千万级以上高Elasticsearch全文向量混合百万级中高在制度问答这个场景里几百份文档切出来也就几十万个向量用pgvector足够了因为它可以直接复用企业现有的PostgreSQL实例少一套组件就少一类故障。如果数据量真到了上亿级别、有多租户隔离和动态索引的硬需求再迁到专业向量库也不迟。另外还有一个很容易被忽略的点不要只做向量检索。在制度和FAQ这类场景里关键词精确匹配的价值经常被低估。员工问“请假单编号规则”如果文档里确实存在短语“请假单编号”BM25的命中往往比embedding还要准。成熟的方案是混合检索向量召回加BM25召回再用rerank模型统一排序。第一版可以先做纯向量但设计时一定要给混合检索留好接口否则以后每改一次都要动整条链路。2.4 服务层与并发FastAPI是AI应用的稳定底座AI服务本质上是一堆慢接口一次生成请求可能持续几秒甚至几十秒。服务框架的选择上FastAPI目前算得上是社区共识原生异步、Pydantic校验、自动生成API文档配合Uvicorn部署也简单。接口设计遵循一个固定套路入参只有用户问题、历史上下文、可选的过滤条件出参必须包含answer、citations和request_id。下面的伪代码就够说明问题class ChatRequest(BaseModel): question: str history: list[dict] [] scope: str | None None # 例如只看某个部门的制度 class ChatResponse(BaseModel): request_id: str answer: str citations: list[str]request_id和citations在第一个版本就必须做出来这两个字段决定了你后面做可观测性和引用追溯时到底有多从容。很多团队到了线上才开始补这些那就不是填几行代码的事了。3. 一个能落地的代码骨架从文档入库到回答生成这一章直接给出可复制的项目结构和核心模块划分然后逐个模块讲清楚“为什么这么做”。3.1 目录结构按领域划分而不是按技术分层AI应用最大的特点就是改动频繁文档格式会变、检索策略会调、模型会换。如果目录按“controller/service/dao”这种传统Web三层去组织你的每次改动都会同时散落在多个包里。更合理的做法是按领域模块切分。我当时用的骨架是这样的intranet-qa/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── api/ # 路由层只做协议转换 │ ├── core/ # 配置、日志、中间件 │ ├── modules/ │ │ ├── ingest/ # 文档解析、清洗、切块、入库 │ │ ├── retrieval/ # 向量检索、混合检索、重排 │ │ └── generation/ # prompt组装、模型调用、引用校验 │ ├── schemas/ # 请求/响应模型 │ └── services/ # 跨模块编排 ├── tests/ │ ├── golden_set/ # 回归测试集 │ └── test_pipeline.py ├── scripts/ │ ├── eval_runner.py # 批量评测 │ └── build_index.py # 全量重建索引 ├── configs/ │ └── settings.yaml └── pyproject.tomlmodules下的ingest、retrieval、generation三个包各自负责数据接收和数据返回的结构定义。后续文档格式改动、检索策略调整、模型替换都能控制在一个包内部不用伤筋动骨。这是我从多次重构里换回来的经验。3.2 文档入库清洗比切块更重要很多教程只讲切块策略却囫囵跳过文档清洗这是本末倒置。我们第一次直接拿PDF跑管线入库的文本里混着大量扫描件乱码和页眉页脚。员工问“年假有多少天”模型不知道从哪条页脚里看到了“年假”两个字就自信满满地把页脚标成了引用来源。这类问题出现一次业务部门对整个系统的信任就要打一个大折扣。清洗环节至少要覆盖这几步扫描件走OCR把图像转成可检索文本识别失败的部分单独标记不要混进正式库去掉页眉页脚、页码、重复标题表格转成“字段值”的纯文本格式统一编码处理特殊符号和乱码字符这一步即使只有千分之一的脏数据也会在向量检索里被放大提取章节路径把一级标题、二级标题、三级标题串成层级路径作为块元数据。切块策略上直接按固定字数切是最省事也最坑的。我们优先按文档里的标题层级切Word的Heading1/2/3就是天然边界块过大才递归往下切切的时候保留完整段落绝不让一个句子断在两个块的中间。每个块至少记录doc_id、章节路径、来源URL、更新时间。切块的代码骨架长这样def split_document(sections, max_tokens600): chunks [] for section in sections: if len(section.text) max_tokens: chunks.append(section) continue parts split_by_paragraphs(section.text, max_tokens) for part in parts: chunks.append(section.with_text(part)) for chunk in chunks: chunk.prefix build_section_path(chunk.title_path) return chunks这里的关键是chunk.prefix它把“这是来自《休假管理办法》第三章第二节的内容”这段层级上下文拼到了每个块前面。模型生成时能看到这个上下文回答自然更贴合制度口径溯源也更简单。3.3 检索增强生成把“问”拆成“查”和“答”RAG的底层逻辑一句话就能说明白不让模型凭记忆回答先给它一批材料再让它基于材料作答。但在工程实现上我习惯拆成四步查询改写、召回、重排、生成。第一步是查询改写。用户问“产假要提前多久申请”文档里写的很可能是“申请产假需提前”。直接用口语原句做向量检索召回效果往往一般。我们用轻量模型把口语问题改写成一个更正式的检索query同时抽出关键词。做这个动作之前我也犹豫过觉得多一次调用多一份成本和延迟但实测数据是不改写时召回命中率约83%改写后到92%从此它成了固定工序。第二步是召回。向量检索TopK取20如果已经做了混合检索就加上BM25的结果。这个阶段的原则是“宁可多召回不能漏召回”因为漏了再好的生成模型也无济于事。第三步是重排。Top20直接全塞进prompt既超窗口又稀释注意力所以要用一个rerank模型把Top5筛出来。我对重排的感受很深它带来的正确率提升往往比换一个更大的模型还明显。原因是重排把真正相关的片段挤到了上下文更核心的位置。第四步是生成。核心代码可以精简成下面这样def rag_answer(question: str) - Answer: query rewrite_query(question) # 查询改写 hits hybrid_search(query, top_k20) # 混合召回 reranked rerank(question, hits, top_n5) # 重排 context assemble_context(reranked) # 保留章节路径 answer generate(question, context) # 生成 return Answer(textanswer, citationsreranked)3.4 答案格式与引用标注让每个回答都可追溯对制度问答这类业务引用不是锦上添花而是硬性合规要求。第一版我们就让模型输出带编号的答案比如“根据《休假管理办法》第3.2条产假需提前15天申请[1]”。但请记住不要天真地认为prompt里写了“请标注引用”模型就会照做。必须加一个生成后的规则校验用正则提取答案里的[n]标记检查每个n是否落在本次检索Top5的范围内如果某一句结论性的话没有任何引用标记就触发一次更严格的重新生成或者直接把这句话过滤掉。实测里这个后处理步骤让用户投诉“AI瞎编”的频率大幅下降。它比多写二十行prompt都管用。4. 从“能跑通”到“能交付”评测、降级、可观测性很多团队能在一周内把RAG demo跑通但半年都推不上线问题就出在这三个环节上。4.1 先建回归集再做功能这个顺序不要搞反几乎所有失败的AI项目都有一个共同特征开发期全凭人肉看几个例子判断好坏上线一改数据就崩。我的做法是动手开发功能之前先把手头真实的用户问题整理成一个golden set。数量不用多50到100条就够但必须覆盖三类典型提问、含糊提问、故意用旧制度口径提问的“钓鱼题”。评测指标也分成三类分开考量检索命中率答案所依据的片段是否真的存在于知识库答案正确率人工抽检或LLM裁判打分引用准确率引用来源与答案内容是否一致。LLM作为裁判不是不能用但要注意它的癖好裁判模型对“回答是否流畅”很敏感对事实错误却很钝感。所以我们的做法是先自动比对人工标注的硬性事实点再用裁判模型做综合评分两个分数分开记录。一个回答“写得流畅但事实全错”的badcase在纯裁判模型方案里很容易漏掉。4.2 降级链路不能让一个AI接口拖垮整个系统生成模型的延迟波动和偶发故障是常态不是异常。所以系统一出生就要预设好当模型真的崩了用户会看到什么我们设计了三档降级完全正常RAG回答并带引用生成超时或报错降级为返回命中的文档片段列表前端展示“AI助手暂时不可用以下是为您找到的相关文档”检索也异常返回固定话术把用户引导到人工工单。第二档很好用因为检索模块的稳定性远高于生成模块。切换逻辑用超时时间控制比如主生成链路3秒没返回就触发降级。注意第三档的话术必须产品化用户不是工程师不能看到500错误码直接给“请稍后重试或联系行政服务台”这样正常的话。降级之外语义缓存是一个性价比极高的优化。员工问“年假怎么算”和“年假规则是什么”本质上是一次计算。我们把改写后的query向量化先去缓存表里找相似度超过0.9的历史问题命中就直接返回已有答案省掉一次完整调用。在真实场景里缓存命中率能做到20%到30%成本下降相当可观。4.3 可观测性每个请求都要能被追回来AI应用的可观测性比普通Web应用多两个维度prompt内容和token用量。很多线上case如果看不到之前那次请求的prompt和response根本无从定位。我们的结构化日志固定包含以下字段字段说明request_id全链路追踪IDquery原始问题rewritten_query改写后的检索queryretrieval_hits召回片段ID及分数rerank_hits重排后的片段IDanswer模型最终输出citations最终引用列表model模型名称与版本tokensprompt_tokens与completion_tokenslatency_ms各阶段耗时cache_hit是否命中语义缓存这些日志不仅是排错工具也是一个免费的badcase矿藏。我每周会导一次日志按“用户重问”“回答超时”“引用为空”这几个条件各筛一遍筛出来的记录就是下周优化最重要的输入。4.4 性能调优先压首字延迟再提吞吐用户对AI应用的耐心跟对搜索引擎差不多两秒还没出字就开始烦躁。性能优化优先级应当是先降首字延迟再提整体吞吐。第一版我们就做了流式输出用SSE把首个token尽快推到用户面前避免一个HTTP请求默默等上十几秒。并发这块重点是连接池复用和超时设置自部署模型还要做索引预热把向量库索引和模型权重提前加载到资源池里避免高峰时段的冷启动。冷启动的等待时间往往比用户能接受的上限还要吓人。5. 实测中踩过的坑完整的排查链路这一章写几个我真实踩过的坑。每个坑我都会给“现象→排查过程→根因→修复”的完整链路而不是直接甩一句答案因为排查思路本身比答案值钱。5.1 答非所问先查检索再查生成现象用户问“月度绩效怎么评定”AI回答了一堆职级晋升的内容。第一反应往往是prompt没写好但这类问题80%的根因在检索环节不在生成环节。排查过程先看日志里的retrieval_hits发现Top5里根本没有《绩效管理办法》的内容只有一篇PDF的扫描件片段。再单独跑一次向量检索确实召回不到“绩效”相关的答案。进一步检查入库日志发现那篇PDF是扫描件清洗管道里的OCR环节跑挂了整个文档以乱码形态进了向量库。修复OCR流程重新入库问题彻底消失。这个case给我最大的教训是日志必须把“生成之前”和“生成之后”切开记录。你只有看到检索阶段召回了什么才能快速判断锅在检索还是生成。很多人一上来就改System Prompt改十遍也没用因为问题根本不在那。5.2 引用乱标根因通常在切块现象AI回答的内容正确但引用来源标错了——内容明明出自《考勤管理办法》它却标了《休假管理办法》。这个现象很有迷惑性因为从答案内容看没问题业务方通常只是随手反馈一句“来源不对”。排查过程先检查模型的输出prompt发现模型引用的chunk元数据标题确实是《休假管理办法》但chunk实际包含的正文后半段内容属于《考勤管理办法》。再回到入库代码发现问题出在切块环节固定max_tokens的切块器在某个边界把两篇文档的内容拼接到了同一个chunk里向量化后的文本语义混杂元数据只取了标题就导致了张冠李戴。修复方案切块时严格按文档边界隔离禁止跨文档拼接每个chunk只保留单一标题路径。这个坑让我彻底想明白了一件事切块的边界直接决定引用的可解释性。你在这一步偷懒后面引用校验和数据追溯都会加倍偿还。5.3 首日调用量超预算成本估算的错误示范现象系统上线第一天模型调用成本是预估的4倍。预算超得这么离谱不是模型单价变了而是成本模型从一开始就算错了。回到预算方案当初只按“一次问答一次生成调用”来算完全漏掉了查询改写、rerank、embedding、fallback重试这些隐性调用而且并发峰值比平均调用量高一个数量级。正确的成本模型至少要考虑一次问答实际触发几次模型调用可能包含改写、生成、失败重试embedding和rerank的调用次数以及缓存是否生效并发峰值与平均值之间的倍数系数用峰值去预留成本而不是用平均。打个具体的比方假设日活1000人人均10次问答一次问答需要两次模型调用每次调用约1500 token输入加300 token输出一天的token总量就是一个不小的数字再乘上峰值系数成本很容易膨胀数倍。我们的对策是三个动作同时做语义缓存先把重复问题吞掉查询改写换成便宜的小模型上线初期限制单个用户的调用频率。三个动作完成之后成本掉了60%。5.4 沉淀成排查清单下次照单抓药踩过这些坑之后我把排查步骤沉淀成了一张固定清单。每次线上反馈故障我按这个顺序过一遍基本十分钟内能定位80%的问题是否命中缓存命中就先检查缓存里的旧答案是否过期查retrieval_hits正确答案是否在召回列表里不在查文档清洗和向量检索正确答案在召回列表里但生成内容不对查prompt、上下文长度和模型温度输出内容对但没有引用查引用校验规则和后处理逻辑以上全部正常但用户仍投诉大概率是查询改写引入了歧义重点看rewritten_query字段。这张清单现在已经固化成了团队内部每一次AI问题排查的标准动作。如果现在让我重新从零做一个AI工程项目我会把顺序压缩成一句话先把检索和评测做得足够扎实再考虑模型调优。很多团队把顺序完全搞反了模型换了一个又一个检索却还是一团糟这是最不划算的技术投入。希望这篇从选型、代码骨架到排错思路的全过程能给你一些实际参考。如果你正在做类似的知识库问答系统欢迎来聊聊你在清洗、引用校验或成本控制上踩过的坑我一定会告诉你我们当时是怎么绕过去的。