news 2026/9/29 3:37:47

AI工程实战:从零搭建稳定可靠的文档问答Agent系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI工程实战:从零搭建稳定可靠的文档问答Agent系统

AI工程(ai engineering)这两个词放在一起,最近被讨论得越来越频繁。很多人以为它会提示词就能算懂AI工程,实际真正上手之后才会发现,提示词只是最表层的东西,背后还站着数据准备、结果稳定性、成本控制、效果评估、Agent状态管理、外部工具调用——这些才是工程的核心。如果你正打算把AI从"偶尔用来查资料"升级成"能稳定跑在日常业务里的能力",或者想系统地搭一套属于自己的AI工作流,这篇文章就是讲这些事。我会从头拆一个实际的AI工程项目,把从零到可用的完整路径、每一步的取舍逻辑、踩过的坑都讲清楚。

1. 项目拆解:AI工程到底在解决什么问题

1.1 为什么"从零开始"是一个好起点

先有一个共识:AI工程不是一门单独的学科,而是一套把大模型能力稳定嵌入到真实产品里的方法论。"from scratch"这个起点很重要——因为没有历史包袱的时候,你可以重新审视每一个环节,而不是在旧系统上打补丁。

我在最初接触这套体系时,犯过一个典型错误:一上来就奔着"做个大而全的Agent"去,结果被各种问题淹没——模型回答不稳定、上下文被截断、外部接口超时、花的小时费不知道用在了哪。后来调整思路,从一个非常小的闭环开始:输入一个需求文档,输出一份规范的需求拆解。这足够简单,但已经包含了AI工程最核心的几个要素:输入处理、上下文组织、模型调用、结果校验、失败重试。

这个"最小闭环"就是整个工程的地基。把它做稳了,后面加什么都有依托;反过来,如果地基不稳,Agent再花哨也是空中楼阁。

1.2 先搞清楚AI工程与提示词的本质区别

如果只写一个提示词就能解决问题,那这件事不值得做工程。提示词是你对一次会话的描述,AI工程是你对一套系统的设计。两者关键差异在三个地方:

  • 提示词面对的是一次性对话,AI工程面对的是重复发生的业务过程。既然重复发生,就必须定义输入格式、输出格式、异常分支。
  • 提示词靠模型自由发挥,AI工程必须用代码约束关键路径。比如模型返回的不是合法JSON,怎么处理?核心信息缺失,怎么补?这些都要写进工程逻辑。
  • 提示词不关注成本和延迟,AI工程必须算清每一轮调用消耗了多少token、平均延迟是多少、月成本曲线长什么样。

一句话概括:提示词是写给模型看的,AI工程是写给系统看的。系统需要表现稳定,就不能允许"这次运气好答对了"的情况存在。

2. 核心能力地图:从提示工程到Agent系统

2.1 提示工程不是玄学,而是受控的上下文设计

我的习惯是先把提示词工程拆成四个可控维度:角色设定、任务边界、输出约束、上下文组织。

角色设定决定模型的语气和立场,但不必写得花哨。重点在于让它明确"你是谁、你在什么场景下为谁服务"。任务边界要写清楚"做什么和不做什么",这个比很多人想象的更重要。我见过大量案例,模型答跑题不是因为能力不够,而是因为提示词里没写"如果信息不足,直接说不知道"。输出约束则是工程化最关键的部分——让它输出结构化格式(JSON、Markdown表格、固定模板),否则下游解析代码会崩溃。

上下文组织是提示工程里最值得投入的部分。模型不是"看到什么就记住什么",而是"在有限的注意力里选择性地利用什么"。你要做的是帮它圈定重点:把最相关的信息放在离问题最近的位置,冗余信息要么删掉,要么放到一个单独的"参考材料"区并注明"如需使用再查阅"。这个技巧我在实际项目里反复验证,效果提升往往比换更大模型还明显。

2.2 Agent编排:一个任务要拆成几步走

当任务复杂到一次提示词无法覆盖时,就该上Agent了。Agent的本质是"多轮决策 + 工具调用",它把一个大任务拆成阶段:理解需求、规划步骤、调用工具、校验结果、输出答案。每个阶段都是一个独立模型调用,并且有代码参与决策。

但我要提醒一个容易踩的坑:Agent不是越多轮越好。每多一轮调用,就多一次延迟、多一份成本、多一个失败点。正确的做法是"能一步做完就不做两步,能用检索解决就不靠思维链绕弯"。

具体到编排层的设计,常用的是两种思路:

  • 链式编排:任务按固定顺序执行,适合流水线场景,例如"读取文件 -> 摘要 -> 生成周报 -> 格式化输出"。特点是简单可控,适合对稳定要求高的场景。
  • 图式编排:任务节点之间存在分支和回退,适合探索型任务,例如"先尝试方案A,不满足条件再切换到方案B"。特点是灵活,但需要引入状态管理和循环控制。

从零开始做,我的建议是先走链式编排,把所有节点跑通之后,再根据实际瓶颈引入分支。一上来就搭复杂状态图,调试成本会高到让你怀疑人生。

2.3 工具链:模型负责推理,代码负责确定

有一句在工程圈流传很广的话:模型是用来处理模糊的,代码是用来处理精确的。这意味着,凡是能写进代码的逻辑,就不要指望模型去推理。举个例子:你要从用户输入里提取日期,不要让模型自己去理解"下周三"这种表达再转成时间戳,更稳的做法是模型只负责抽出"下周三"这个文本片段,时间转换交给专门的日期解析库处理。

这套分工方式也决定了Agent工具链的组织方式。我通常会把外部能力封装成一个个独立函数,每个函数有明确的入参、出参和错误处理,再通过函数注册机制交给模型选择调用。模型只负责"决定调用哪个函数、传什么参数",执行是什么结果、要不要重试,全部交给代码层控制。

这里还要强调一个观点:不要盲目地把API都做成Agent工具。工具越多,模型选错的概率越大,提示词里的工具说明占用的token也越多。我一般会把类似功能的工具合并,同时在工具说明里写清楚"什么时候不要用这个工具",这能显著降低误调用率。

3. 实战案例:从零搭建一个文档问答Agent

3.1 需求定位与最小可行性设计

我选了一个典型到大家都能感同身受的场景来做完整演示:一个团队内部的项目文档问答系统。输入是任何团队成员提问,输出是"引用文档原文 + 基于原文的结论 + 不确定的说明"。这个需求简单,但完整覆盖了AI工程的全链路。

先把需求拆成优先级:最核心的一条是"回答必须有据可查"。这意味着系统不能只靠模型记忆回答,而要通过检索把相关文档片段找出来放进上下文。第二条是"找不到答案时要明确说不知道",不能编。第三条才是响应速度和成本。

基于这几点,我给系统定了三层架构:文档清洗层、检索层、生成层。每一层独立,可以单独替换。这个设计在后面迭代里帮了大忙——换了不同模型、调整了检索策略,其他层完全不需要动。

3.2 落地步骤:从检索到生成的完整链路

第一步是文档准备。很多初学者忽略这一步,直接拿原始PDF和Word去建索引,结果检索效果惨不忍睹。需要先做清洗:去掉页眉页脚、统一编码、按章节拆分。我强烈建议按"标题层级 + 段落语义"双重规则拆分文档,而不是粗暴地按固定字数截断。每个chunk保持在500到800字左右,这样既能保证语义完整,又不会让检索结果太零散。

第二步是向量化与存储。用Embedding模型把每个chunk转成向量,存入向量数据库。这里的关键参数是Embedding模型的选择和向量维度,它决定了检索的召回质量。实测下来,用中等大小的中文编码模型在多数场景下比通用的英文模型好很多,代价是向量维度稍高、存储成本略增,但检索准确率的提升完全值回票价。

第三步是检索。用户提问后,把问题也转成向量,用余弦相似度找到最相关的Top-K个chunk。这里有个细节:Top-K不能拍脑袋设。设3太小,容易漏关键信息;设8太多,大量无关片段会稀释模型的注意力。一般建议先设5,根据实际回答质量再做微调。我在项目里做的优化是"混合检索"——向量相似度检索和关键词匹配并行,再把结果融合排序。因为文档里大量专业术语本身是低频词,单靠向量检索很容易漏,关键词检索能弥补这个盲区。

第四步是生成。把检索到的chunk按照相关度排序后拼接成上下文,再加上系统提示词和用户问题,一起交给生成模型。提示词里我会特别加上三条约束:只基于给定上下文回答;如果上下文不足以回答,明确说"文档中未找到相关信息";回答中引用对应文档编号。这个"引用编号"的设计是后期溯源排查的命脉,少了它,出问题你根本不知道是哪段文档误导了模型。

下面是检索生成核心环节的简化代码实现,用的是最常见的OpenAI兼容接口,你换成任何国产模型服务也只需要改base_url和model参数:

import os from openai import OpenAI client = OpenAI( base_url=os.getenv("LLM_BASE_URL"), api_key=os.getenv("LLM_API_KEY") ) def retrieve_docs(question, top_k=5): # question_vec = embed_model.encode(question) # 从向量库取回候选chunk列表,并按相关度排序 # 为方便演示,这里假设已通过函数返回 return search_top_k(question, top_k) def build_context(chunks): parts = [] for c in chunks: parts.append(f"[文档编号 {c.doc_id}] {c.text}") return "\n\n".join(parts) def answer_question(question): chunks = retrieve_docs(question) context = build_context(chunks) sys_prompt = """你是企业内部文档助手。 回答必须严格基于给定的文档内容。 如果文档中找不到答案,直接回复:文档中未找到相关信息。 请在每个关键结论后用[文档编号]标注引用来源。""" user_content = f"文档参考:\n{context}\n\n问题:{question}" resp = client.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=[ {"role": "system", "content": sys_prompt}, {"role": "user", "content": user_content} ], temperature=0.2 ) return resp.choices[0].message.content

需要说明的是,上面的search_top_k只是一个占位函数,实际项目中它背后是向量检索加关键词检索的融合逻辑。代码的重点在于整个生成链路的清晰划分:上下文怎么构建、提示词怎么约束、结果怎么返回,每一步都可以单独替换和测试。

3.3 评估与回归:让效果可量化的关键打法

做AI工程最怕的就是"感觉效果好"和"感觉效果差了"这种主观判断。我一开始也走过弯路:改了一版提示词,测了几个问题觉得不错,上线后却发现另一些问题的回答质量明显下降。从那以后,我把评估做成了工程的一部分。

具体做法是建立一套评测集。按业务场景收集至少30到50个典型问题,每个问题都标注标准答案或评价要点。然后每次改动之后,跑一遍评测集,记录通过率和关键失败案例。评测集分三类:

  • 正向题:文档里能明确找到答案的题目,目标是通过率尽可能接近100%。
  • 反向题:文档里根本没有答案的题目,目标是模型必须拒绝回答而不是编造。
  • 边界题:答案需要跨多个chunk拼接的题目,这类最能暴露检索和组织的弱点。

用这套评测集,你能非常直观地看到改动的影响。有一次我把Top-K从5调到8,正向题通过率上升了6个点,但反向题误答率上升了3个点,说明增加了上下文冗余,模型开始"自作聪明"地从无关内容里找联系。这就是评估的价值——不靠感觉,靠数据说话。

4. 工程化细节:性能、成本与稳定性

4.1 成本控制:token消耗的数学账

AI工程和大模型API打交道,成本是绕不开的话题。很多人的成本失控不是模型贵的锅,而是设计的时候没算账。先说一个基本公式:一次完整问答的成本约等于(输入token数乘以输入单价加输出token数乘以输出单价)乘以调用次数。看着简单,但输入token这块隐藏的消耗远比你想象的大。

做个简单的计算:假设每轮对话输入模板加上中间结果约2000 token,输出平均500 token。如果模型输出价是输入价的3倍,一次问答成本大约等于输入2500 token的等价。每天跑一万次问答,月成本就是2500乘以10000乘以30再乘以单token价格。再乘个重试系数1.15,这个数可能就已经超出预算了。降低成本的思路不是砍功能,而是减少无效token消耗:

  • 缓存固定模板和系统提示词,避免每次重复发送相同内容。
  • 给Agent任务设置最大步数,防止模型陷入无限循环。
  • 对长上下文做精简,检索时只把最相关的chunk送入模型。
  • 把不需要执行复杂推理的环节切到更便宜的小模型上,主打模型专门负责最后生成。

4.2 稳定性设计:重试、降级与熔断

外部模型服务不会因为你项目着急就变得稳定。超时、限流、返回空值、返回乱码,这些都是常态。我在项目里必做三件事:

第一,重试策略。对瞬时错误最多重试两次,采用指数退避,例如第一次等1秒,第二次等2秒。对持续性错误(比如鉴权失败、账户欠费)不重试,直接报警让人去处理。

第二,结果校验。模型输出必须经过一道程序化校验。如果是要求JSON的输出,就真的去解析;缺字段就抛异常,触发修复逻辑或重新生成。不要相信模型每次都信守承诺。

第三,降级与熔断。当模型服务连续报错,或者单次请求延迟超过设定阈值,系统要能自动切换到一个兜底方案,比如返回提示"该功能暂时不可用,请稍后再试"。真正生产环境里,主模型故障时自动切到备用模型,总比整个功能瘫痪要好。这不是防御过度,而是你辛苦搭好系统之后,最现实的保命手段。

4.3 数据安全与合规红线

做AI工程,尤其处理企业内部文档时,数据的流动边界必须在设计阶段就想清楚。这里有两条必须守住的底线:

第一,不上传敏感数据到无法审计的外部服务。所有发送给模型服务的数据都应当经过脱敏和授权检查。我在系统中专门加了一层"出网前检查",把明显包含敏感信息的内容拦截下来。

第二,日志与追踪要留痕。每次模型调用、每个检索操作都要有日志,记录入参和出参。这不是技术洁癖,而是真正出事的时候,唯一的排查依据。实践中发现,很多AI应用上线后一旦回答出错,排查根本无从下手,原因就是调用链没有日志。

说到日志,这里要提一个关键角色:trace追踪。在一次Agent多轮调用里,你需要完整记录每一步的输入输出、耗时、token数,可视化地回溯"模型为什么做出这个选择"。市面上常见的LangSmith、Langfuse等追踪工具我也试用过,我的结论是有比没有强太多,选型倒是次要的。你在自己的项目里哪怕用最简单的日志表,把trace_id、step、model、input、output、cost几个字段记录下来,就已经超过大部分初学者。

5. 常见问题排查与避坑实录

5.1 幻觉、漏答与上下文截断——三个最常见的翻车点

幻觉是AI工程被吐槽最多的点。大模型本身的生成机制决定它总是要说出点什么,就算没有依据。对抗幻觉不能只靠提示词加"不要编造",那是心理安慰。真正的解法是从架构上留出"不知道"的出口:检索不到相关内容时直接进入拒答分支,根本不把生成任务交给模型。同时让模型引用来源,一旦回答没有引用,你就要当心它是否在自由发挥。

漏答则和检索有关系。我遇到过几次,文档里明明白白写了答案,系统却说没找到。排查后发现是拆文档时把完整的上下文截断了,答案被拆成两半,检索只取到一半。解决办法是chunk之间保留重叠区,让每个chunk带上一点前后文信息,避免语义断裂。

上下文截断又是另一类问题。prompt越长,能容纳的输出空间就越小。我遇到过调用了很多工具、塞了一大堆中间结果之后,模型无法正常输出结构化结果的情况。这类问题的排查思路首先是减少输入,而不是盲目加大请求上限。

5.2 排查清单与速查表

基于我的实战经验,把AI工程中最常出现的问题、可能原因、验证方法和解决思路整理成一张速查表。下次再遇到类似问题,先对着表看,而不是乱改提示词。

问题现象可能原因验证方法解决思路
回答和问题完全无关检索系统召回的海量chunk基本不相关,上下文被污染先打印检索命中的chunk列表,检查其相关性调整检索参数,引入关键词检索或增加相关性阈值过滤
输出不是想要的格式,偶尔合法偶尔乱格式约束只写在提示词中,没有程序化校验连续测试10次,统计格式合法率增加输出校验函数,非法结果触发修复或重试
模型照旧编造答案拒答规则只写在提示词中,没有架构兜底用反向题集测试误答率检索置信度低时直接走拒答分支,不调用生成
回答质量整体不稳定温度参数太高,导致每次随机性过大同一个问题重复测试5次,观察差异把temperature降到0.1到0.3之间
cost异常飙升输入中放入了大量无关内容或循环调用过多查看trace日志中的token消耗统计优化上下文结构,限制Agent最大步数
请求经常超时或报错外部模型服务不稳定或并发超限观察错误码是否集中在限流类接入重试加降级逻辑,错峰调用

5.3 关于"AI工程能力变现"的个人观察

开头给的这个标题我练了很多遍,最大的体会是:它可以同时是一条求职路线、一块个人能力的拼图,也可以是做小而美工具的基础。

这项能力最直接的一个价值,就是把一个人从"使用者"变成"构建者"。同样用AI,当你具备工程化能力之后,你能搭建的是可以被别人使用、可以反复迭代的自动化系统,而不是一次性的对话记录。这样的能力在团队里是稀缺的,作为个人项目经验也会非常有说服力。其实很多时候,"别人被琐事缠身,你用AI搭了一套自动化处理流程,专注核心价值"这一点,就已经是很好的落地故事了。

我的建议是,不要一上来就想着一夜之间做出什么颠覆性的产品,把这种能力当作一个可以持续积累的技能栈,在实践中做出一个又一个小的自动化工具来证明自己。一个能写周报的、一个能查文档的、一个能处理报表的,每个完成之后你都会对工程化有更深的理解,累积起来的信心比任何教程带给你的都多。

写在最后,说说我自己的套路

文章到这里,核心内容基本都铺开了。最后分享几个我在实操中反复验证的经验,送给准备从零开始的人。

第一,先学会"结构化思考AI任务"。任何需求来了,先拆输入、处理、输出、异常四个模块,不要急着写提示词。提示词永远只是处理模块的一部分。

第二,构建一个最小但完整的Demo要远比啃完整套文档重要。我对接过很多新人,最明显的分水岭就是"能不能跑通一个端到端的闭环"。哪怕是让系统回答三个问题,也要保证从文档清洗到检索到生成的链路是通的。跑通之后再迭代,效率完全不同。

第三,坚持记录每次改动的效果。提示词改动、检索参数调整,全部记录下来并与评测集对照。没有数据积累的AI工程,就像没有仪表盘的飞机,飞起来全凭感觉。

这个领域技术迭代确实很快,今天好用的工具可能下个月就被替换。但底层的方法论是值钱的:拆解问题、设计链路、控制成本、评估效果、处理异常,这套工程思维一旦建立,任何新工具出现时你都能快速上手。我个人从零到真正可用,走过的弯路比顺利的路多得多,希望把这些总结出来能帮你少踩几个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 3:37:42

迪普防火墙安装调试实战:三步开局与五个排错技巧

简介:迪普防火墙安装调试步骤借鉴文档面向网络工程师、系统运维人员及防火墙初学者,旨在帮助读者系统掌握迪普防火墙从初始配置到安全策略启用的完整流程。文档以实际调试为主线,详细覆盖VLAN划分与接口IP设定、安全域规划、静态路由配置、DH…

作者头像 李华
网站建设 2026/9/29 3:37:29

ClaudeCode编程助手配 TaoToken:settings.json 骨架与智能编码全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 3:35:56

ScrollView滑不动?五步排查法从测量到事件分发彻底解决

1. 别急着改代码,先搞懂ScrollView为什么会“锁死”1.1 ScrollView滚动究竟靠什么先说一个我自己的经历。这个ScrollView无法滑动的问题,前前后后困扰了我一年多。第一次遇到是在一个商城项目的商品详情页里,外层是ScrollView,里面…

作者头像 李华
网站建设 2026/9/29 3:34:42

让 Claude Code 越写越像你:用 Hook 自动积累编码规范的实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华