ARTICLE DETAIL

资讯详情

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

AI工程从零开始:构建受控可靠的AI Agent实践指南

AI工程从零开始:构建受控可靠的AI Agent实践指南 我自己最初看到“ai-engineering-from-scratch”这个标题的时候第一反应是又一个AI入门教程。但真把它拆开看“from scratch”这四个字说的其实是另一件事——不靠现成的低代码平台、不靠抄别人的Prompt模板而是把一条AI应用从模型选型、上下文设计、工具调用、工作流编排到评估迭代完完整整地自己搭一遍。这几年“提示工程prompt engineering”被讲烂了“AI Agent”又被炒得太热真正能把这两件事串起来并且以工程方式落到业务里的实践却不多。这篇文章想把这条链路里我认为最关键的部分展开讲一下偏向实操不适合完全没写过代码的朋友但如果你有一点点Python基础、熟悉基本的API调用应该能从头到尾跟下来。内容主线就是一件事怎么从零开始搭一个“不让模型裸奔”的AI应用也就是最近常说的Harness Engineering。与其看一百个花哨的Agent演示不如自己亲手把地基打牢。1. 为什么“从零开始”值得做一遍1.1 别把AI工程和“接SDK”画等号很多人以为AI工程就是申请个API Key、填好地址、把用户问题塞进Prompt然后拿着返回结果展示一下。这个认知会让项目在第一版跑通之后迅速卡壳。原因很简单一次成功的调用和一条可靠的产品链路之间隔着一大堆非功能性问题——上下文塞不下怎么办模型输出多了Markdown标记怎么办工具调用的参数被模型编错了怎么办用户连续问十轮之后回答质量掉一半怎么办这些问题在你直接拖低代码节点或者套别人现成框架的时候往往是被“黑盒”藏起来的。框架帮你处理了一部分但也让你失去了判断问题的能力。比如很多新一代的Agent框架确实能自动完成“规划→调工具→再生成”的循环可一旦某个环节出错日志里只有一层看不懂的抽象你只能对着框架源码发呆。我从零搭过一次之后最大的收获就是再也不会被这些底层细节吓住——因为每一条消息是怎么进出的、工具结果是怎么回传给模型的我都亲眼看过。1.2 “from scratch”到底是在解决什么问题以业务视角看老板或者客户从来不关心你用了什么框架他们要的是“某个任务被可预期地完成”。从零开始搭建时你要做的不是训练模型而是把现有模型的能力约束到特定任务上。这个“约束能力”的过程就是工程。Harness Engineering这个说法最近被频繁提起直译是“驾驭工程”意思很形象模型是一匹很有力气的马但你不能让它想往哪跑就往哪跑。你要给它戴上笼头、系好缰绳——笼头是清晰的工具权限和输出格式约束缰绳是最大步数、校验规则、降级逻辑和人工审核点。Prompt只是缰绳的一部分远远不是全部。想明白这一点你就不会再迷信那些所谓“一句咒语让模型变聪明”的文章了。1.3 工程化与单次调用的本质区别单次调用只关心“我这个Prompt这一次返回了什么”工程化关心的是“这个系统连续跑1000次失败率是多少、失败了怎么处理、每次花多少钱、效果怎么度量”。这两种思维模式决定了完全不同的工作方式。单次流写Prompt → 跑一次 → 看着结果不错 → 结束。工程流定评测集 → 定指标 → 写第一版代码 → 跑回归 → 改Prompt或逻辑 → 再跑回归 → 加日志 → 上灰度。工程流听起来繁琐但它是唯一能在团队协作中活下去的模式。没有评测和日志你两周前调出来的“神Prompt”可能某天换个模型版本就悄悄变残了而你根本发现不了。所以这篇文章后面会把“评估与回归”单独拿出来讲因为它是把AI项目当工程做的分水岭。2. 先拆链路AI工程的五段主线2.1 模型选型先把参数和成本算清楚选模型不是选“最强”而是选“合适”。我从实际经验出发建议至少在三个维度做对比指令跟随能力、上下文窗口、单次成本。下面这张表是我自己对常见任务的粗选参考。模型档位上下文窗口指令跟随成本量级典型场景轻量模型8K-32K中很低分类、抽取、格式化、FAQ问答中端模型32K-128K高中客服、写作助手、多轮对话高端模型128K-200K很高高复杂推理、长文档分析、代码生成本地小模型2K-16K中低硬件成本隐私敏感、离线场景成本账一定要提前算。假设你的应用每天有1万次请求每次输入2000 token、输出500 token那么一个输入输出单价都在几十元/百万token级别的模型一个月下来的推理成本就在千元量级如果模型单价高一个数量级成本也会跟着跳一个数量级。所以实践中我更倾向于“混合策略”简单任务走轻量模型难任务才上高端模型中间加一层意图判断来分流。这种分流逻辑本身就是一种Harness——不让高成本模型处理不该它处理的事。2.2 上下文工程把信息塞进模型能用的形态大模型能“看到”的只有上下文窗口所以上下文工程是整个AI工程的第一层地基。很多人的误区是“上下文越长越好”实际上并非如此——关键信息密度比总长度更重要。往系统里塞大量无关背景模型不但不会变聪明反而容易被干扰项带偏回答质量肉眼可见地下降。我在实践中的上下文组织方式大概是这样系统提示词固定放任务定义、角色、硬性规则、输出格式要求。历史消息按最近优先滑动窗口保留超出窗口后做摘要压缩。工具定义放在系统提示词之后让模型先知道“手里有哪些工具”。用户输入最新问题放最后确保模型注意力集中在最新指令上。顺序也很重要。基于我自己的对比测试把“任务目标”放在前面、把“本次要处理的具体内容”放在末尾效果通常比反过来更好。你可以把这个机制理解成给模型发工作指令先说单位和岗位职责再下发今天的任务单而不是把任务单扔在一堆旧文件底下。2.3 提示工程不等于写Prompt模板提示工程最容易被误解成“文案优化”这完全是两码事。写好Prompt的关键不是辞藻华丽而是结构化表达。一个相对完整的Prompt至少包含六个部分角色定位、任务描述、输入数据、约束条件、输出格式、示例。我在实际项目里会明确要求模型“不要解释、不要客套、直接输出结果”。与Response格式相关的部分能给出Schema就给Schema能用代码块包裹的样例就给样例尽量让输出可被程序解析。下面是一个“坏写法”和“好写法”的对比坏写法“帮我看看这段新闻是正面还是负面输出一下结果。”好写法你是情感分析助手。 任务判断输入文本的情感极性只允许输出positive/neutral/negative。 约束不要解释理由如果文本为空输出empty。 输入{text} 输出格式JSON对象格式为 {sentiment: positive|neutral|negative|empty}这样改完代码层可以直接解析JSON字段不需要再去清洗多余的“这句话表达了……”。另外必须强调一点Prompt约束是软约束模型不是每次都老实听。所以你必须在代码层加一道硬校验解析失败就重试或降级而不是指望模型自觉。2.4 Agent与工具调用让模型有手有脚AI Agent和我们平时聊的“对话机器人”最大区别在于它能把一个复杂任务拆成几步并且通过工具调用来改变环境——查数据库、发请求、调计算器、写文件。这个能力确实很酷副作用也很大模型会在一个错误的路径上自信地跑很远。Harness Engineering在Agent场景里体现得最明显。我在搭Agent时会给它套上四层约束工具权限只暴露当前任务真正需要的工具不要把“删文件”“写任意路径”这种高权限操作直接交给模型自主调用。最大步数设置迭代上限比如最多调5次工具超了就停。结构化工具定义每个工具要有清晰的名称、描述和参数Schema让模型容易正确调用。可观测日志记录模型每一步的决策、工具返回、报错信息方便回放问题。很多失败的Agent项目失败点往往不在“模型不够聪明”而在“模型被允许做太多事情又没有人盯着”。这就像放一个能力很强的实习生独自处理客户投诉却不给工作手册、不给审批流程、不记录他做了什么——再聪明也会出乱子。2.5 评估与回归工程化的起点没有评估就没有工程。这句话听起来像口号但我是踩过坑之后才真正认同的。早期我调Prompt基本靠肉眼自己试几条觉得“嗯看起来不错”就上线。直到有一天我把某个Prompt改得更“顺滑”之后单条看起来没问题整个评测集的平均分却悄悄掉了15%当时没有任何机制能发现这件事。所以现在我的做法很简单先建立最小的评估闭环准备20到50条典型输入覆盖正常请求、边界请求、无关联请求。定义通过标准格式是否合法、答案是否命中关键事实、工具调用是否正确。每次修改Prompt或代码都跑一遍回归把结果和上一次对比。自动评估可以用“规则校验 LLM裁判”双通道。规则负责客观项比如JSON是否合法、是否包含指定字段“LLM裁判”负责主观项比如答案是否流畅、是否贴合用户意图。两套结果合并成一份报告一眼就能看出改动是变好还是变坏。后面第3章我会给一个缩小可用的例子。3. 一个最小可运行的实操案例从零搭一个带工具调用的AI Agent3.1 场景定义与模块拆分这一节用一个很小的场景串一遍完整链路做一个“查天气并生成出行建议”的AI助手。用户可以输入“北京明天适合跑步吗”系统先让模型调用天气工具拿到数据再根据天气生成建议。选这个场景是为了说明问题它足够简单不牵涉真实的复杂业务但又完整覆盖了工具调用、多轮消息传递、输出生成这三个关键环节。整体架构拆成四层接口层负责和模型服务通信统一处理超时、错误和重试。调度层维护对话上下文判断模型是要调工具还是直接回答。工具层定义并执行具体工具负责把结果转换成模型能读取的内容。模型层调用大模型传入系统提示、历史消息和工具定义。模块拆分的目的是让每一条链路都具备“单独替换”的能力。以后想换工具、换模型、改Prompt都只动对应模块不会一把梭全改。3.2 模型调用层先做一个能统一收口的封装先封装一个兼容OpenAI格式的请求函数国内外的模型服务大多也兼容这个格式。这里把配置放在环境变量里不写死在代码中。import os import json from openai import OpenAI client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL), ) def chat(messages, toolsNone, modelgpt-4o-mini, temperature0.2): params { model: model, messages: messages, temperature: temperature, } if tools: params[tools] tools resp client.chat.completions.create(**params) return resp.choices[0].message封装的好处很直接以后要加日志、统一超时、换模型服务只需要改这一个文件。实际项目里我会在函数内部再加一层记录调用耗时和Token消耗的日志逻辑方便算成本。3.3 工具层与函数调用让模型“看得到也用得上”先定义一个天气查询工具。真实项目里可以替换为任何天气API这里用模拟数据演示重点是让工具定义符合模型可读的格式。def get_weather(city: str, date: str today) - dict: # 实际项目中替换为真实天气服务调用 mock_data { city: city, date: date, weather: sunny, temp_c: 26, wind_level: 3, } return mock_data tools [ { type: function, function: { name: get_weather, description: 查询指定城市在指定日期的天气情况, parameters: { type: object, properties: { city: {type: string, description: 城市名例如北京}, date: {type: string, description: 日期格式YYYY-MM-DD默认当天} }, required: [city] } } } ]工具定义有三个关键点名称要短且语义明确描述要写“这个工具能做什么、什么时候用”参数Schema要完整。如果你的参数有固定枚举值就在description里写清楚否则模型很容易发挥出你预料之外的值。接下来写一个简单的分发器根据模型给的工具名和参数调用对应的Python函数。def dispatch_tool(name: str, args: dict): if name get_weather: return get_weather(**args) raise ValueError(funknown tool: {name})这个分发器可以继续扩展成注册表模式项目大了之后新工具只需要注册不用改主流程。3.4 工作流编排主循环到底该怎么写工具调用不是一次性搞定而是要跑一个主循环模型如果返回了tool_calls我们就执行工具并把结果回传模型如果没有tool_calls说明它准备直接回答这时候就返回内容。核心代码如下SYSTEM_PROMPT 你是出行助手。根据查询到的天气信息给用户提供简明、可执行的出行建议。 约束 1. 先调用天气工具获取数据再生成建议。 2. 建议控制在三句话以内不要编造工具未提供的数据。 3. 如果用户问题与天气无关回复“我只会回答与天气、出行相关的问题”。 MAX_STEPS 5 def run_agent(user_input: str): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for step in range(MAX_STEPS): msg chat(messages, toolstools) if msg.tool_calls: # 这一步非常关键必须原样追加上一条带tool_calls的消息 messages.append(msg) for tc in msg.tool_calls: # 解析参数解析失败时要把错误回传给模型 try: args json.loads(tc.function.arguments) result dispatch_tool(tc.function.name, args) except Exception as e: result {error: str(e)} messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse), }) else: return msg.content return 已达到最大处理步数请简化问题或稍后重试容易踩坑的地方有两处。第一带tool_calls的assistant消息必须原样追加回去因为后续的tool结果消息需要和tool_call_id一一对应丢了这条模型会报错第二工具参数解析必须放到try-except里否则模型一旦生成非法JSON整个流程就崩了。把错误信息作为工具结果回传给模型让它自己修正是实用性很强的容错思路。3.5 加上评估与日志从“能跑”到“能迭代”先给系统加日志。日志不用搞得很复杂每次请求记录模型、输入/输出Token数、耗时每次Agent运行记录完整的工具调用序列和最终输出。这样出了问题能回放“模型当时看到了什么、做了什么决定”。再加一个简易评估脚本准备好测试用例对每个用例跑一遍run_agent然后把结果写入统计JSON。test_cases [ {input: 北京明天天气怎么样适合跑步吗, must_contain: [北京], should_have_tool_call: True}, {input: 你好, must_contain: [无关], should_have_tool_call: False}, {input: 上海今天下雨吗, must_contain: [上海], should_have_tool_call: True}, ] def evaluate(): results [] for case in test_cases: output run_agent(case[input]) results.append({ input: case[input], output: output, tool_call_happened: get_weather in str(output), passed_keyword_check: any(k in output for k in case[must_contain]), output_length: len(output), }) return results这个例子只用了“关键词命中”和“是否调用了工具”这类规则指标已经能帮你挡住很多回归。后续要做更精细的评估就再加上LLM裁判打分针对“回答是否合理”给一个1到5分的评分。3.6 扩展成更复杂的Harness工程模块化是根本上面这个最小系统跑通之后你可以按四个方向扩展加记忆把多轮对话的关键结论写入内存或向量库下一轮再注入上下文。加检索让工具里多一个“搜索知识库”的函数把相关文档片段取回后压缩进上下文。加人工审批对高风险工具调用如提现、删除、发送消息插入审批节点模型生成请求后挂着等确认。加降级链主模型超时或失败时自动切换到备选模型连续失败时直接走人工接管。我在实际项目中体会到第一版“从零”不需要特别花哨把每个环节做成可观测、可替换的模块后续扩展就是“插积木”而不是“换地基”。4. 常见问题与排查技巧实录4.1 模型输出格式飘忽不定请求JSON却给你Markdown这是最常见的坑。你明明在Prompt里写了“只输出JSON”模型还是时不时给你一段带json代码块的内容或者前面加一句“好的以下是结果”。原因在于Prompt对模型来说是软约束不是硬校验。正确做法是让“硬校验”发生在代码层。我的处理流程先把模型输出清洗一遍比如去掉代码块围栏再用json.loads解析解析失败时把报错信息追加到消息里让模型自己修正。实测下来大多数情况下重试一轮就能拿到合法JSON。如果重试两轮还失败就返回一个默认兜底结果而不是把异常抛给用户。4.2 上下文塞满后回答质量肉眼可见地下降很多项目一开始跑得挺好用户多聊几轮之后回答就开始“失忆”甚至胡说。这是因为上下文窗口被历史消息占满了新指令被稀释在大量旧内容里。排查方法非常直接打印一下每次请求的消息条数和Token数如果每次都把全部历史带上窗口满了之后必然劣化。我建议的兜底方案是滑动窗口加摘要只保留最近5到10轮原始消息更早的内容让模型压缩成一段摘要再放进系统提示里。这既能保留对话主线又不会让有效信息密度快速下降。另外工具返回的数据不要原封不动塞回去能精简就精简只留模型生成回答真正需要的字段。4.3 函数调用参数解析失败模型生成的arguments不合法模型生成的arguments字段理应是JSON字符串但偶尔会带上注释、多一个逗号、或者把字符串值写得不带引号。这个问题在小模型上出现的概率明显更高。我踩过这个坑之后的经验是三层兜底第一层try-except捕获解析异常第二层把异常信息回传给模型让它重新生成第三层是给每个工具设置合理的默认值即使参数有缺漏也能返回一个可用的结果而不是直接报错。这三点做到位之后工具调用的可靠性会提升一个档次。4.4 评估指标定不出来什么都想评什么都评不准初级项目最容易犯的错是“凭感觉判断回答好不好”导致每次修改Prompt的结果只能靠肉眼对比。破局方法是分两步走先定客观可校验的指标再定主观评分通道。客观指标包括输出JSON是否合法、关键字段是否缺失、是否调用了预期工具、输出长度是否超标。主观评分用LLM裁判把用户输入、系统输出、评分标准一起喂给一个更强的模型让它输出1到5分的分数和一句简短理由。评测集里一定要覆盖边界输入——空输入、超长输入、无关输入、带攻击性的输入。这些边界输入往往最能暴露系统缺陷。4.5 问题排查速查表下面这张表是我自己整理的一套排查顺序出了问题先对照着看能省不少时间。运行现象可能原因排查线索解决建议偶发返回格式非法Prompt约束较弱查看失败时的原始输出清洗重试兜底默认值多轮对话后变笨上下文窗口塞满打印Token数与消息条数滑动窗口历史摘要工具参数解析失败模型生成非法JSON查看tool_calls的原始字符串try-except错误回传重试长时间没人发现效果变差缺评测集与回归翻历史版本改动记录建立最小回归集调用量上来后成本失控没有模型分流统计各模型Token消耗按任务难度拆分模型工具执行返回大量冗长数据工具结果未经精简查看注入上下文的大小只保留关键字段结合我个人带项目的经验上面这些坑大概率会按“格式问题→上下文问题→工具问题→评估问题”的顺序出现。每解决一个系统就会向“可交付”靠近一大步。最后再分享一个小的个人心得。我实际把AI工程从零搭过一遍之后最大的改变不是“我会调接口了”而是对系统里每一个环节都有掌控感。以后换模型、加工具、改Prompt我都会先跑一遍测评看差异而不是直接上线让用户替我踩坑。如果你也想搭一套自己的AI应用我的建议很简单第一版不用追求复杂把单轮工具调用跑通配上日志和一个二十条用例的评测集再开始加花活。地基打不牢模型换得再勤也没有用。
返回列表