☰
AI工程从零开始:Prompt、结构化输出与RAG实战路线
2026/11/3 22:22:33 网站建设 项目流程

去年秋天有个做业务的同事找我,说想做一个"能自动读合同、把关键条款抽出来"的小工具。我嘴上说"这个简单",心里想的其实是:这不就是把合同丢给大模型,让它总结一下吗?结果真坐下来动手,才发现自己对这个问题的理解浅得可笑。合同要分页解析、条款要分类型提取、输出要严格按固定字段来、抽错了还得有办法发现——这一串事情加起来,根本不是"调一下AI"能糊弄过去的。

那时候我才意识到,"AI工程"这个词,跟我原本以为的完全不是一回事。它不是让我们去训练一个模型,也不是简单调用一下API接口,而是站在"模型能力"和"真实业务"之间的那一整个工程层:上下文怎么做、输出怎么约束、错了怎么发现、成本怎么控制。今天这篇东西,就是想把我从零开始摸这条路的经验整理出来,给同样想入行的人一个能落地的路线参考。

1. "AI工程"这四个字,一年前我理解错了

1.1 从一次答应得太快的需求说起

先把上面那个合同项目讲完。我最初的做法特别幼稚:把合同全文拼进Prompt,然后问"请提取以下字段"。

第一轮跑出来的结果看起来不错,条款都在,格式也像那么回事。但仔细一核对就露馅了:付款日期提取错了,同一个字段在不同合同里表述不一样;有的合同几十页,上下文塞不下;最离谱的是,模型在合同里根本没有的内容上"脑补"了一个违约金比例。这四类问题,每一类都指向一个完全不同的工程点——上下文管理、结构化输出、输入预处理、以及幻觉控制。

这个经历教会我一件事:AI工程的重点不在"模型有多聪明",而在"你用什么机制让一个不确定的系统稳定地输出正确结果"。模型是那个发动机,但方向、换挡、刹车都得工程来管。

1.2 AI工程和算法岗、传统开发的边界在哪里

很多人(包括当时的我)分不清AI工程和另外两个角色的区别,我直接做个对照:

角色核心对象典型产出判断成败的标准
算法/研究岗模型本身训练方法、模型权重、论文指标涨没涨
传统后端开发确定性的代码逻辑接口、系统、数据库功能对不对、稳不稳
AI工程模型能力与业务之间的连接层应用、Agent、RAG系统、评测体系业务结果稳不稳定、成本合不合理

AI工程最特殊的地方在于:你操作的是一个非确定性系统。同一个Prompt,同一个输入,两次调用结果可能不一样。传统开发里"bug能复现"这条铁律,在这里不成立。你得学着用校验、重试、兜底、评测这类手段,把不确定性关进笼子里。

所以我不建议一上来就抱着深度学习教材啃。AI工程"从零开始",零的不是数学基础,而是工程化思维——怎么把一个API变成一条可信赖的生产链路。这是两套完全不同的技能树。

2. 零基础起步的技术栈取舍:先学什么,后学什么

2.1 Python和API就是第一课,不是大模型原理

如果有人问我"AI工程从零开始第一步学什么",我的答案永远是:Python基础语法、HTTP请求、JSON处理、以及读懂一份API文档。就这么朴素。

你不用先会反向传播,甚至不用先懂Transformer结构。绝大多数AI工程工作发生在模型之外:拼Prompt、处理文档、调接口、解析返回值、写评测脚本。这些全是Python基本功。

我见过不少朋友一上来就去啃PyTorch教程,学了一周张量运算,然后发现自己要做的东西跟这些完全没关系。不是说原理不该懂,而是知识获取的顺序错了——先动手把链路跑通,产生真实反馈,再回头补原理,效率高得多。

2.2 绕不开的五个底层概念

不管用哪家模型,这几个概念是通用的,我建议刚开始就把它们彻底搞明白:

  • Token:模型计费和上下文长度的单位。英文大约1个词等于1-2个token,中文1个汉字大约是1-2个token。它决定了你的输入能塞多满、每次请求花多少钱。
  • 上下文窗口:模型一次能看到的全部内容上限,包括系统提示词、用户输入、历史对话和模型输出。超出部分不会报错,而是被静默截断,这是最阴险的坑之一。
  • 温度与采样参数:temperature控制随机性。做提取、分类这类任务我通常设0或接近0;做生成类内容才调高。
  • 角色体系:chat模型区分system、user、assistant三个角色,系统提示词是"给模型的劳动合同",优先级最高。
  • 结构化输出与函数调用:让模型按约定格式返回或触发工具,这是把模型从聊天机器人变成"员工"的关键开关。

这些概念都不难,但每个都对应着一类生产事故。比如不理解上下文窗口,就会在长文档场景里莫名丢信息;不理解温度,就会在做抽取任务时得到飘忽不定的结果。

2.3 我的推荐学习顺序:Prompt → 结构 → RAG → Agent → 微调

把学习路径压缩成五站,每一站都建立在前一站之上:

  1. Prompt工程:学会写系统提示词、给示例、设约束。这是地基,也是性价比最高的技能。
  2. 结构化输出:学会用JSON模式、函数调用把模型的"话"变成程序能直接使用的数据。
  3. RAG检索增强:学会把私有知识、长文档通过"切分-向量化-检索-拼装"的方式喂给模型。
  4. Agent编排:让模型自主决定调用哪些工具、按什么顺序执行。这是当前上限最高也最难的一站。
  5. 微调(可选):当Prompt和RAG都调不动了再考虑。对绝大多数业务,前四站已经够用。

我特别想强调:前期别急着上框架。LangChain这类框架确实能帮你少写代码,但它同时把"到底发生了什么"藏在抽象后面。我第一次用框架跑RAG,出了问题根本不知道数据流在哪一环断的。后来改成手写全链路,几十行代码,所有环节清清楚楚,再回去用框架就顺手多了。先用最笨的方式做一遍,再做优化,是AI工程里最值得坚持的习惯。

3. 第一条生产线:把Prompt变成可复用的函数

3.1 提示词不是写作文,是定义接口

很多新人写Prompt像在写作文,动辄几百字,有情有景有要求。但真正生产级的提示词,更像在定义接口:输入是什么、输出是什么、边界在哪里、出错时怎么办。

以提取合同条款为例,差劲的Prompt是"请帮我提取这份合同里的关键信息";合格一点的Prompt会先定义角色和任务边界,再给输出字段的精确schema,再给2-3个"输入-输出"示例,最后声明"如果原文中没有该字段,必须返回空字符串,不得自行推断"。

三条切实可用的经验:

  • 系统提示词里写任务是做什么、不做什么,边界比能力重要。"不要做的事"往往比"要做的事"更能防止幻觉。
  • 少样本示例要选困难的、有代表性的,不要选最顺的。让模型看到"边界情况长什么样",比给它一堆标准答案有用。
  • 输出格式的约束写两遍:一遍在文字描述里,一遍放在结构化输出参数里(如果有),双保险。

3.2 用JSON模式锁定输出格式

让模型自由发挥文本,然后你解析——这个思路在Demo里可以,在生产里会把你折腾死。模型输出"是的""好的没问题"这种废话时,你的正则、JSON解析器全都会崩。

现在主流模型基本都支持JSON模式或结构化输出。我自己的做法是:把schema传给模型,让它在schema约束下输出,然后再在代码里用pydantic或json库做二次校验。注意,校验这步不能省——模型承诺了JSON,不代表它真的会给合法JSON,网络层截断、字符转义错误,都可能导致解析失败。

这里贴一段我当时写的提取函数的核心逻辑:

from openai import OpenAI import json client = OpenAI() contract_schema = { "type": "object", "properties": { "party_a": {"type": "string"}, "party_b": {"type": "string"}, "contract_amount": {"type": ["number", "null"]}, "payment_terms": { "type": "array", "items": {"type": "string"} }, "missing_fields": { "type": "array", "items": {"type": "string"} } }, "required": ["party_a", "party_b", "contract_amount", "payment_terms", "missing_fields"] } def extract_contract(text: str) -> dict: resp = client.chat.completions.create( model="gpt-4o-mini", response_format={"type": "json_object"}, messages=[ {"role": "system", "content": ( "你是合同条款提取引擎。只提取原文中明确存在的信息。" "原文没有的字段,contract_amount返回null,missing_fields列出缺失字段。" "不得推断、不得补全、不得修改原文措辞。" )}, {"role": "user", "content": text[:8000]} ] ) data = json.loads(resp.choices[0].message.content) # 二次校验:字段缺失时抛出异常,而不是带病进入下游 assert "payment_terms" in data, f"missing payment_terms: {data}" return data

看到那个assert了吗?我一开始没有它,后来因为某个模型的输出偶尔丢字段,下游程序直接崩了,报错信息跟实际原因毫无关系,排查了整整一下午。加上这行校验之后,问题当场暴露,清清楚楚。

3.3 函数调用:让模型从"聊天"变成"干活"

结构化输出解决的是"怎么说",函数调用解决的是"做什么"。让模型在对话过程中主动选择调用你预定义的函数,这是所有Agent类应用的基础。

举个最简单的例子:做一个客服助手,它需要查询订单状态、处理退款申请。我不再让模型自己"猜答案",而是给它两个工具,让它判断什么时候该调用工具、传入什么参数:

tools = [ { "type": "function", "function": { "name": "query_order", "description": "查询订单当前状态,需要订单ID", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "用户提供的订单号"} }, "required": ["order_id"] } } }, { "type": "function", "function": { "name": "refund_order", "description": "发起退款,需要订单ID和退款金额", "parameters": { "type": "object", "properties": { "order_id": {"type": "string"}, "amount": {"type": "number"} }, "required": ["order_id", "amount"] } } } ]

调用流程也很固定:把工具列表传给模型,模型返回一个"工具调用请求"而不是普通文本;你在代码里执行对应函数、拿到真实结果;再把结果作为新的消息回传,让模型基于真实数据继续回答。这个"模型决策-程序执行-结果回填"的循环,就是Agent的最小内核。

这里必须提醒一句:凡是涉及真金白银的操作(退款、删除、发送消息),工具执行权一定要做人工确认层。模型判断错了工具参数是常有的事,我在测试里见过它把退款金额"四舍五入"成整数。这种问题不是模型能力能解决的,是流程设计要兜底的。

4. 手写一个最小RAG系统:知识库问答的完整闭环

4.1 什么场景真正需要RAG

先泼一盆冷水:不是所有知识问答都该上RAG。如果知识量少、更新不频繁、能塞进上下文窗口,直接拼进去就行,又简单又便宜。

但当出现下面任一情况时,RAG就从可选变成必选:

  • 知识总量超过上下文窗口(几十页、几百页的文档);
  • 知识频繁更新,不可能每次都重新接进模型;
  • 需要明确"答案出自哪份文档"的溯源要求。

RAG的核心思想一句话能说清:不要逼模型记住所有知识,而是在每次提问时,先从一个外部知识库里检索出最相关的片段,把它作为参考资料塞进Prompt,再让模型基于参考资料回答。模型负责"读"和"写",知识库负责"记"。

4.2 文档切分:最容易被低估的一步

整个RAG链路里,切分(chunking)是最不起眼、但对效果影响最大的环节之一。切太大,检索命中一堆无关内容;切太小,上下文被割裂,语义不完整。

我踩过的坑:一开始按固定300字符切,结果很多段落被从中间劈开,导致检索到的片段"前言不搭后语"。后来改成按段落边界切、每段设一个最大长度、相邻块之间留一点重叠,效果好了一个档次。具体参数没有标准答案,我建议至少试三组配置——小块(200-300字)、中块(500字)、段落级+重叠——然后用同一批测试问题去看检索命中率,别靠感觉。

4.3 向量化、检索、重排、生成:四步走的最小实现

下面这段代码,是我用最原始的库(不依赖任何框架)搭起来的最小RAG,每一步都看得见摸得着:

import chromadb from openai import OpenAI client = OpenAI() chroma_client = chromadb.Client() collection = chroma_client.create_collection("docs") # 第一步:文档切分(按段落切,超长段落再二次切) def chunk_text(text: str, max_len: int = 500) -> list[str]: paragraphs = [p.strip() for p in text.split("\n") if p.strip()] chunks, current = [], "" for p in paragraphs: if len(current) + len(p) <= max_len: current += p + "\n" else: if current: chunks.append(current.strip()) current = p + "\n" if len(p) <= max_len else p[:max_len] if current: chunks.append(current.strip()) return chunks # 第二步:入库时生成embedding并保存 def index_document(doc_id: str, text: str) -> None: chunks = chunk_text(text) embeddings = [] for c in chunks: emb = client.embeddings.create( model="text-embedding-3-small", input=c ) embeddings.append(emb.data[0].embedding) collection.add( ids=[f"{doc_id}-{i}" for i in range(len(chunks))], embeddings=embeddings, documents=chunks, metadatas=[{"doc_id": doc_id} for _ in chunks] ) # 第三步:检索+拼装Prompt def ask_with_rag(question: str) -> tuple[str, list[str]]: q_emb = client.embeddings.create( model="text-embedding-3-small", input=question ).data[0].embedding hits = collection.query(query_embeddings=[q_emb], n_results=3) contexts = hits["documents"][0] system = ( "你是一个知识库问答助手。只能依据参考资料回答;" "如果参考资料中没有答案,明确说'知识库中未找到',禁止编造。" "回答末尾标注信息来源编号。" ) user = "参考资料:\n" + "\n---\n".join( f"[{i+1}] {c}" for i, c in enumerate(contexts) ) + "\n\n问题:" + question resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "system", "content": system}, {"role": "user", "content": user}] ) return resp.choices[0].message.content, contexts

这个版本只能算"能跑",但它把RAG的每个环节都摊开了。等你理解了这个闭环,再考虑引入重排模型(Rerank)、混合检索(关键词+向量)、父子分块这些高级技巧。我有一个很深的感受:直接上手高级方案的人,出了问题往往说不清是哪一环坏了;而手写过最小闭环的人,看到任何框架都能一眼看穿它的数据结构流。

4.4 第一次跑通后,先别庆祝,做这三件事

第一,把检索结果打出来肉眼检查。我见过太多人只盯着最终答案,完全不看模型"读"了什么。如果检索到的片段跟问题毫不相关,答案再漂亮也是空中楼阁。

第二,换一组切分参数重跑一遍。用同一批问题、同一个知识库,只调整chunk大小和重叠值,对比答案质量。你会惊讶地发现,同样的系统,差别只在于怎么切文档,效果能差出好几个量级。

第三,建一个至少20条的测试问题集。每个问题标注"预期答案要点"和"应该引用的文档"。这是后面做评估的原始材料,越早开始积累越好。

5. 上线前绕不开的坎:评估、成本与不稳定输出

5.1 先建一个30条的评测集,再谈上线

AI应用最大的特殊性在于:它没有"改完就能上线"这回事,你永远需要回答一个问题——"这次改动到底是变好了还是变坏了?"没有评测集,你只能靠感觉,而感觉在非确定性系统面前是最不可靠的东西。

我的做法是拿30-50条真实问题当固定测试集,每次改动都跑一遍,记录三类指标:

  • 检索质量:正确文档有没有被召回(召回率),排序是否合理。
  • 答案正确性:核心事实有没有答对。这个可以用"LLM打分"辅助评估,但核心case我一直坚持人看。
  • 格式合规率:能不能被程序无损解析。这个前面说过,是生产事故的重灾区。

评测集不用一次建完,但一定要从第一天就有。我后来养成的习惯是:线上每出现一次错误回答,就把这个case回收进评测集。这样评测集会越来越接近真实世界的难度,而不是只覆盖你"想得出来"的用例。

5.2 Token预算、延迟与成本:动手算一笔账

很多人做AI应用最不看重的就是钱,直到账单寄来才懵。其实成本估算非常简单,动手算一次就记住了。

举个具体的例子:假设你的RAG系统每次请求消耗约3000个输入token + 500个输出token,用某款常见的商用模型,输入价格按每100万token 2.5元、输出按每100万token 10元算(价格是示意,以官方为准),那么单次请求的成本大约是:

  • 输入:3000 / 1000000 × 2.5 = 0.0075元
  • 输出:500 / 1000000 × 10 = 0.005元
  • 单次合计:约0.0125元

听起来很便宜对吧?但如果你的应用有10万日活、每人每天调用20次,单日成本就是0.0125 × 2000000 = 25000元。算法没变,只是乘了个DAU。所有AI应用的商业模式,都必须过这一道算数题:单次交互的成本 × 调用频率 × 用户量,能不能被收益覆盖。

延迟是另一本账。链路里每多一次串行调用,延迟就线性叠加一次。常见的大模型单次响应需要1-3秒,如果你的Agent流程要串行调用4次模型,用户感知的等待就是4-12秒。优化手段无非三板斧:能并行的并行、能缓存的缓存、能砍掉的调用坚决砍掉。先算清楚瓶颈在哪,再动手优化,不然就是瞎忙。

5.3 面对"模型今天状态不好":用工程手段把飘移按住

模型输出的不确定性是绕不开的,但工程上有一整套办法把它按住。我在生产里常用的防御手段:

  • 温度调到最低:凡是抽取、分类、工具调用类任务,temperature一律设0或最小允许值。
  • 加一层输出校验器:对模型返回做schema校验,不合格就重试一次,重试仍失败就走兜底文案。
  • 日志记录一切:每次请求的Prompt原文、模型输出、解析结果、耗时、成本全部落日志。没有日志,出问题就像在没有监控的机房修服务器。
  • 对高危操作加确认层:前面提到的退款、删除类操作,一律由程序拦截、人工确认后执行。

这四条看着不起眼,但它们共同构成了一道"安全网"。AI工程做到后期,比的不是谁调出来的效果更惊艳,而是谁的系统在模型抽风的时候还能输出正确结果。

6. 踩坑半年后回头看:给想走这条路的人几句实在话

6.1 先用最笨的方式跑通,再谈框架和优化

这句话我反复对自己说。很多人一上来就研究LangChain、研究Agent编排平台、研究微调框架,研究了半个月一行业务代码没写。我的经验是:先用裸API把最小功能做出来,哪怕丑、哪怕慢,跑通之后你对"数据从哪进、从哪出"就有了肌肉记忆。这时候再去学框架,你是在"用"它,而不是被它牵着走。

6.2 你写的不是"提示词",是系统的边界

Prompt里最值钱的往往是那句"不要做什么"。给模型划定禁区,比教它做什么更能防止事故。我在做合同提取时加的那句"不得推断、不得补全",直接让脑补类错误大幅下降。把这段话放在系统层面理解:AI工程的所有代码,本质上都是在画边界——Prompt是边界,校验是边界,人工确认层也是边界。

6.3 跟业务方聊需求,先问"错了怎么办"

传统软件开发聊需求先问"要什么功能",AI工程聊需求得多问一句"做错了怎么办"。合同字段抽错了能不能发现?客服答错了由谁兜底?推荐内容不合适有没有投诉通道?这些问题的答案,直接决定系统的架构设计。很多AI项目死在验收环节,不是因为模型不聪明,而是因为**"错误"这件事从来没有被设计进系统里**。

最后分享一个我自己的小习惯:每次上线新功能,我会故意准备一批"刁钻输入"去测——特别长的、特别短的、格式乱成一团的、包含诱导性内容的。因为大模型系统里,最不可预测的风险恰好藏在这些没人会准备的地方。AI工程这条路没有终点,但打好地基之后,每往前走一步都能看到新的东西。希望这份经验能帮你少踩几个我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询