ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零构建AI工程:从模型调用到工具编排的完整实践指南

从零构建AI工程:从模型调用到工具编排的完整实践指南 “ai-engineering-from-scratch”这句话我在不少技术讨论区里见过有人理解为“从零手写神经网络”也有人当成“从零搭一套AI应用”。我自己的定位偏后者不依赖某个大而全的框架而是从最底层的工程视角把一个AI系统的各个零件——模型通道、提示词、工具调用、记忆管理、效果评估——一个个亲手搭出来让整个过程可复现、可调试、可替换。今天这篇就来聊这件事。想写这个题目是因为这两年总有人问我我已经会写Prompt了也知道怎么调API为什么一到做产品就卡住答案通常不在某个模型或某个库上而在“工程”这两个字里。一个能跑起来的AI应用要处理的不只是“生成一段话”还有上下文怎么管理、工具调用怎么容错、结果怎么验证、成本怎么控制。这篇文章会以我做过的实际项目为例把从零构建AI工程的完整链路拆开讲。适合正在做Agent、做AI工作流、或者打算把大模型接入业务的开发者参考刚跨进AI方向、想建立系统认知的工程师也能从中找到一条能落地的路径。1. 先想清楚AI工程到底在解决什么问题1.1 AI工程不是“调模型”很多人把AI工程的重心放在模型选择上总以为换了更强的模型问题就能迎刃而解。现实是模型能力越来越像水电煤真正的难点在模型之外的系统设计。如果你只关心API返回的文本内容那你做的其实不是AI工程是Prompt调用。AI工程要解决的是让模型在复杂、多变、有约束的真实任务里稳定地工作并且随着业务变化可持续演进。我见过一个典型反面案例团队把一份用户需求文档直接丢给大模型让它“总结要点”Demo阶段效果惊艳。一上生产就崩了——用户传100页文档模型开始丢细节问几个追问对话就陷入“你说东它说西”偶尔还输出一堆Markdown格式的废话。这不是模型不行而是整条链路从没按工程标准设计过。输入没有预处理上下文没有截断策略输出没有结构校验失败没有兜底。任何一个环节都足以让系统在真实场景里翻车。AI工程的核心其实就是三件事可控、可测、可演进。可控是指你能预判和限制模型的行为边界可测是对每个关键输出都有办法验证好坏可演进是换了模型、改了Prompt、加了工具之后系统整体行为依然能被稳定比较和迭代。围绕这三件事去做比追求“最强模型”重要得多。1.2 从零构建的AI应用由哪几层组成如果把一个AI应用拆开你会发现它通常包含六层接入层、指令层、工具层、状态记忆层、编排层、评测层。接入层负责跟模型打交道包括API调用、流式读取、重试退避指令层承载你精心设计的系统提示词和输出约束工具层让模型能调用外部能力比如执行命令、读取文件、搜索网页状态记忆层管理对话历史和分析结果避免上下文失控编排层决定整个Agent的循环逻辑什么时机调用模型、什么时机调用工具评测层则是标尺用来判断当前系统是变好了还是变坏了。从零构建时哪些层要自己写我认为核心是编排层、工具协议、上下文管理、评测闭环。模型调用可以用现成SDK向量数据库也可以用现成的但编排逻辑必须你自己想清楚。因为框架能帮你省掉很多样板代码却没法替你定义“业务闭环长什么样”。举个最直白的例子一个Agent在调用工具失败后是把错误信息返回给模型让它换个思路还是直接终止任务不同选择对应完全不同的产品体验。这个决策埋在一个框架的抽象方法里时你很难看清它到底做了什么。我并不是提倡所有项目都重复造轮子。实际上当我手工实现过一套编排循环之后再回头看LangChain等框架的代码理解完全不一样——我知道它每一层解决什么问题也知道哪些抽象对我来说是多余的。如果你刚开始我建议先手工搭一个最小闭环哪怕代码很简陋这个“亲手过一遍”的过程比看十篇框架教程都值。2. 技术选型与架构决策动手前先定四件事2.1 模型通道托管API还是本地推理模型通道是第一个要拍板的决策。托管API优势很明显不用管部署调用即用模型版本由服务商持续更新适合快速验证想法。本地推理模型的优势则体现在数据不出域、长期调用成本可控、可对模型做针对性微调。但本地部署不是光有显卡就行它还意味着要处理并发排队、显存管理、模型更新等一堆运维问题。维度托管API本地推理延迟受网络影响波动较大内网调用延迟稳定成本按token计费高频场景偏贵前期硬件投入大之后边际成本低数据合规数据经过第三方服务数据留在自己环境定制能力仅支持服务商开放的能力可量化、微调、自定义采样运维复杂度低高需要模型服务化、监控、扩缩容我的经验是原型和快速迭代期优先托管API把业务逻辑打磨通顺等系统稳定、调用量上来之后再评估要不要把高频路径迁到本地。不用一上来就追求“全部私有化”。另外现在有个比较务实的玩法是混合通道——把敏感的数据处理任务走本地模型把需要强推理能力的规划任务走托管API两者在统一编排层里切换。这个方案对隐私和成本都能兼顾就是实现时要多做一层路由抽象。2.2 提示词工程第一层“亲手搭建”很多人觉得提示词工程就是“把话说明白”这没错但工程化的提示词要更严谨。我习惯把系统提示词分成四块角色定义、任务背景、输出约束、示例。角色定义告诉模型它是什么任务背景说明它面对什么场景输出约束规定格式和禁区示例则给它几个“标准的答案样子”。你是项目启动助手。你负责根据用户提交的需求描述和项目文件输出一份可执行的项目实施方案。 要求 1. 先分析需求中的核心目标再拆分任务不要直接堆砌任务名。 2. 如果信息不足允许在方案里明确列出“待确认问题”但不要向用户反问。 3. 输出必须是一个JSON对象字段如下 - summary: string方案摘要 - tech_stack: string[]推荐技术栈及理由 - tasks: {name, desc, estimate_hours}[] 4. 不要输出Markdown代码块之外的内容不要输出无关客套话。 示例输出 {summary: 搭建一个带审批流的小型项目管理系统, tech_stack: [Vue3, FastAPI, PostgreSQL], tasks: [{name: 设计数据库表, desc: ..., estimate_hours: 4}]}这段提示词看起来不长但每个约束都有存在的理由。角色定义能显著减少模型“乱入戏”的概率明确“不要反问”是为了让程序能自动处理信息缺失规定JSON结构则是为了方便下游程序解析。提示词工程不是堆砌华丽词藻而是像写接口文档一样把边界条件写清楚。调用参数同样重要温度一般不超过0.3因为Agent场景要的是确定性不需要发散创意max_tokens要设定上限防止模型长篇大论把上下文挤爆。2.3 工具协议让模型能“动手”只有提示词模型还只是个“纸上谈兵”的顾问。要让它在真实任务里发挥作用必须给它工具。工具协议的第一件事是定义清晰的工具描述让模型知道有什么工具、每个工具干什么、参数是什么。大部分托管API都支持函数调用工具定义的格式用JSON Schema描述即可。[ { type: function, function: { name: read_file, description: 读取指定路径的文件内容用于分析项目源码或文档, parameters: { type: object, properties: { file_path: {type: string, description: 文件绝对路径} }, required: [file_path] } } } ]仅仅定义还不够更重要的是执行侧的安全边界。工具名字可以叫read_file但真正执行时必须把路径限制在允许访问的目录范围内凡是执行shell命令的工具都要配白名单和超时限制工具返回的内容也要截断不能一股脑把10万行日志塞回给模型。我见过不少运行事故都是模型根据错误输出反复尝试同一条失败命令白白跑掉几分钟、烧掉几百块额度。要解决这个问题要么限制最大调用步数要么在工具描述里明确写“如果命令失败不要重复尝试换一种方式”。2.4 记忆与上下文管理从零设计状态大模型的上下文窗口是有限资源而AI工程的应用场景里对话和任务都是持续性的。如何管理历史消息直接决定系统在长任务里的稳定性。最简单的方案是滑动窗口只保留最近N轮对话更早的消息直接丢弃。这是最快也最不容易出错的策略适合工具调用场景因为每轮交互的任务相对独立。但缺点也明显——模型会“失忆”丢失早期的用户需求细节。第二种方案是摘要压缩当消息总长度超过阈值时把旧消息交给模型提炼成一段摘要放到新的上下文前面。这种方案可以保留关键信息但摘要本身就是一次模型调用会引入额外延迟和成本。而且摘要会丢细节比如用户早前提过一个很具体的数字要求摘要可能就剩一句“用户提到了一些偏好”。第三种是向量检索把历史消息切片、向量化按需把相关片段取回。这是最灵活的方式适合知识库型应用但实现复杂且检索效果依赖分块和嵌入模型质量。我自己做项目时通常是组合拳滑动窗口保底摘要处理早期全局信息只有知识库场景才上向量检索。这里有个原则能用简单方案解决就不要急着上复杂方案。上下文管理不是越高级越好而是越匹配场景越好。3. 从零实现一个可运行的AI工程原型3.1 选一个“看得见效果”的业务场景理论讲多了容易飘我拿一个实际做过的项目举例构建一个项目启动助手。它的输入是用户的一句话需求和几个项目文件输出是一份项目实施方案包含摘要、技术栈建议、任务拆解。这个场景看起来不复杂但它完整覆盖了信息抽取、推理、结构化输出、工具调用四个关键环节非常适合用来验证AI工程的完整闭环。做完之后这个助手还能继续扩展成“AI项目经理”自动跟进任务进度、生成周报甚至联动外部系统。不过一开始别想那么多先把一个业务闭环跑通。我建议你也这样不要选那种“既要又要还要”的场景选一个边界清晰、输出明确、能一眼看出好坏的场景练手。3.2 搭建最小闭环从输入到任务生成核心代码并不复杂关键是把“模型调用-解析结果-拼接上下文”这个循环写正确。import json from openai import OpenAI client OpenAI() SYSTEM_PROMPT 你是项目启动助手。根据用户需求和相关文件输出项目实施方案。 输出必须为JSON对象字段包括summary, tech_stack, tasks。 def run_assistant(user_input, max_steps5): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input} ] for step in range(max_steps): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, temperature0.2 ) content resp.choices[0].message.content try: result extract_json(content) return result except json.JSONDecodeError: # 输出不规范把错误信息回传给模型要求修复 messages.append({role: assistant, content: content}) messages.append({ role: user, content: 输出不是合法JSON请只输出JSON对象不要添加任何解释。 }) raise RuntimeError(模型多次输出非法JSON)这个循环里有几个容易被忽略但很重要的点。第一系统提示词里的角色和限制要在每次调用时都完整保留不能因为代码写起来麻烦就省掉。第二当模型输出不合格时不要把错误直接抛给用户而是把问题描述反馈给模型让它自我纠正。第三必须设置最大步数防住模型陷入“无限重试”的死循环。3.3 给AI装上“手”工具调用与安全边界只输出方案文本还算不上“工程”因为任务拆完之后还要能落地执行。我给助手加了两个工具read_file用来读取项目代码和文档run_command用来执行构建、测试等命令。实现工具调用的核心是模型返回的不是普通文本而是一个tool_calls请求系统执行工具后把结果以tool角色消息回传给模型。def handle_tool_calls(message, allowed_dirs(/tmp/work,)): if not message.tool_calls: return None tool_outputs [] for call in message.tool_calls: fn call.function.name args json.loads(call.function.arguments) if fn read_file: path args[file_path] # 安全校验只允许读取白名单目录内的文件 if not path.startswith(allowed_dirs): tool_outputs.append({role: tool, tool_call_id: call.id, content: json.dumps({error: 路径不在白名单}))}) else: content open(path).read()[:8000] tool_outputs.append({role: tool, tool_call_id: call.id, content: content}) elif fn run_command: # 只执行白名单内的命令限制超时 import subprocess if not args[command].startswith(python ): tool_outputs.append({role: tool, tool_call_id: call.id, content: 命令不允许}) else: result subprocess.run(args[command], shellTrue, capture_outputTrue, timeout10) output result.stdout[-3000:] result.stderr[-3000:] tool_outputs.append({role: tool, tool_call_id: call.id, content: output}) return tool_outputs这里绝不是随便写的几行代码。路径白名单、命令白名单、输出长度限制、超时限制四个闸口缺一个系统就可能在某个边界情况下搞出大麻烦。我踩过一次坑没做输出长度限制模型读取了一个超大日志文件结果回传内容直接撑爆上下文窗口后面所有对话都开始“晕”。从那以后我给所有工具返回值都统一加上了长度上限。3.4 建立评估体系先有标尺再谈优化没有评估体系的AI项目迟早变成玄学调参。你可能今天觉得模型效果不错明天换了个Prompt调了两轮效果反而更差但因为没有对比基准完全说不清差在哪。所以从构建第一版原型开始就应该搭一个最小评估集。我当时的做法是准备20条测试案例每条都包含用户输入、期望输出的关键字段、以及判定规则。比如输出能否成功解析成JSONJSON里有没有summary字段tasks数量是否大于3每条任务是否都包含name和estimate_hours这种规则容易写成代码每次改动后跑一遍用一条命令看出谁回归了、谁进步了。def validate_output(result, expected): if not isinstance(result, dict): return False if summary not in result or tasks not in result: return False if len(result[tasks]) expected[min_tasks]: return False for task in result[tasks]: if estimate_hours not in task: return False return True规则验证之外还要配人工抽查。机器只能判断“格式对不对”判断不了“方案合不合理”。我每周固定抽5条案例自己看一遍模型的输出质量把问题记下来。这些记录慢慢地成了下一轮优化的方向。没有评测的迭代是在碰运气有评测的迭代才是真正的工程。4. 我在实战中踩过的坑与排查技巧4.1 上下文膨胀导致效果退化这是Agent类应用最常见的问题。一开始对话很流畅任务执行到一半模型开始频繁忘记早期的指令答非所问甚至行为模式都变了。最常见的原因是上下文塞了太多内容挤占了对系统提示词和当前问题注意力。我自己的排查思路分三步第一先把messages数组长度和token量打印出来确认是不是膨胀问题第二检查是否每轮都在往上下文里追加大段工具输出而不是做裁剪第三观察模型是否开始重复提旧信息这是注意力被稀释的典型信号。对策是分层处理让重要的系统指令永远保留中间过程性记录用滑动窗口淘汰真正的长期记忆放到外部存储。4.2 输出格式不稳定的修复重试、容错、结构输出模型虽然聪明但输出偶尔也会抽风。最常见的幺蛾子是明明要求输出纯JSON它偏偏在开头加一句“好的以下是我的回答”或者用Markdown代码块把JSON包起来。这类问题靠“再调一轮Prompt”往往解决得不彻底正确姿势是三重保障。第一层请求时开启JSON Mode或结构化输出前提是模型支持。第二层解析端做健壮性处理自动剥离Markdown代码块标记再做json.loads。第三层如果前两层都失败就启动重试逻辑把上一次的错误输出作为上下文让模型重新生成。def extract_json(text): text text.strip() if text.startswith(): text text.strip() if text.startswith(json): text text[4:] start, end text.find({), text.rfind(}) if start -1 or end -1: raise json.JSONDecodeError(未找到JSON, text, 0) return json.loads(text[start:end1])这段修复代码在真实环境里帮了我很多次。核心思想是不要信任模型的输出格式永远在解析层做防御。4.3 工具循环卡死与错误吞没模型调用工具工具又触发模型继续调用工具这本身是正常逻辑但循环一旦失去控制就是事故现场。最常见的情况是模型调用了一个会长时间运行的命令等待超时超时后错误信息没有正确返回给模型导致系统卡在“半空”状态。更隐蔽的情况是模型反复调用同一个参数不同的命令试图“碰运气”浪费大量时间。我的经验是三层控制。第一每次工具执行都设超时超时即中断把“执行超时”信息作为结果返回给模型让它换思路。第二给整个Agent循环设最大步数比如10步超过后强制停止输出“当前已完成的工作和未完成的部分”。第三在工具描述里显式写“失败后不要重复尝试请换一种方法”这是引导模型行为最便宜的方式。4.4 成本与延迟优化AI工程上线之后最现实的问题就是钱。一个Agent任务往往要调用多次模型如果每次都用最强模型成本很快爆炸。我习惯把任务分级简单抽取和格式化走小模型规划、推理、总结走大模型。这个“模型路由”的逻辑可以是规则也可以让大模型来当路由判断。优化手段适用场景效果Prompt压缩长对话、长文档减少输入token结果缓存相同输入重复出现减少重复计算模型路由任务难度差异大降低单次成本批量处理非实时任务提高吞吐、降低调用次数流式输出用户体验优先降低首token时延我还习惯在日志里记录每次调用的prompt_tokens和completion_tokens按天汇总。钱是一点点漏掉的不量化就永远发现不了问题。4.5 数据安全与合规底线这块必须说而且必须放在很靠前的位置。接入大模型服务时输入内容会在请求过程中离开你自己的服务器所以要对数据的敏感程度有清醒认识。客户身份信息、密钥密码、内部核心代码默认都不能直接发给外部模型。工程上要做三层防护脱敏把敏感字段替换成占位符再进入Prompt权限控制让AI工程只接触它完成任务所必需的数据日志管理避免在日志里记录原始输入和敏感推测结果。提示我见过太多团队为了“省事”直接把完整商业计划书发给模型做总结这非常危险。宁可花时间写脱敏模块也不能把风险后置。5. 从原型到可用系统的扩展方向5.1 多智能体协作什么时候值得上“多Agent”现在很火但它不是银弹。我的体感是单Agent在不同阶段切换角色反而是多数业务场景的最优解。只有当任务内部存在明确且稳定的阶段边界时多Agent协作才有明显收益。比如一个“规划Agent”负责拆解任务一个“执行Agent”负责调用工具一个“审查Agent”负责检查结果三者通过消息队列交换信息。多Agent的真正代价是消息传递机制、任务状态管理、结果归因都变得更复杂调试难度成倍上升。如果你打算上多Agent我建议先把单个Agent的Prompt和工具打磨好再用一个极简的消息协议把它们串起来而不是一上来就引入重型框架。那种“两个Agent互相反驳”的对话流演示很酷但线上系统需要的是稳定不是酷。5.2 评测驱动迭代让改动有据可依到了扩展阶段最值得投入的方向不是加功能而是把评测集做大做实。我后来的做法是把黄金样例集扩展到了200条并且按业务场景分组比如“短需求快速拆解”50条、“多文件项目分析”50条、“异常输入处理”50条、“长对话稳定性”50条。每次改动Prompt或工具逻辑就在这些样例上批量跑一遍输出一份对比报表。这个习惯带来的回报非常直接有一次我想给Prompt加上“去重任务”的新规则跑完评测集发现虽然任务重复率降了但“短需求拆解”场景的通过率掉了8%。看了一眼具体案例发现问题出在模型开始过度合并任务把一个单元测试任务并到开发任务里了。如果没有评测集这种退化可能要上线之后才能发现那代价就大了。5.3 部署与可观测性系统跑起来只是开始原型阶段的代码往往跑在本地交互式地调试。上线前至少要做好三件事一是把编排循环做成可重复执行的程序而不是REPL里的一串命令二是记录结构化日志包含每步执行耗时、模型调用次数、token消耗、工具调用结果摘要三是加异常告警比如连续三次模型调用失败或者工具执行超时率超过某个阈值就推送提醒。我常用的日志格式不复杂给每次会话分配一个session_id按时间顺序记录事件。这样出了问题翻日志就能定位是哪个环节、哪一次调用引起的。可观测性不是给运维部门看的它是你做技术判断的眼睛。没有日志AI系统就像一个黑盒你连它是变笨了还是刚被谁改了配置都不知道。我个人做完这个项目最大的体会是“从零”不是“不用任何库”而是你对每一层都熟悉到能随时替换它们的程度。我先手工实现了一遍编排循环再去翻那些框架的源码理解立刻不一样。另一个小心得是每天都跑一遍黄金样例集比对着单个案例反复调Prompt有用得多。如果这篇文章能给你的AI工程之路节省一些试错成本那就值了。如果你也在折腾类似的系统我的建议很简单——先挑一个自己能完全掌控、只有一个业务闭环的小项目把闭环跑通再考虑扩展。
返回列表