news 2026/11/4 10:26:50

AI工程从零实践:从API调用到稳定Agent服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI工程从零实践:从API调用到稳定Agent服务

"ai-engineering-from-scratch"——从零开始的AI工程。我把这个标题当作自己过去一段时间实践的总纲:不借助现成的Agent搭建平台,不直接套用大而全的框架,从一行Model API调用开始,把一个带工具的AI应用完整地做出来、跑起来、迭代起来。这篇文章就是这段实践的经验沉淀,写给那些想真正理解AI工程链路的人:你知道大模型很强,但你未必清楚从一个API Key到一个稳定可用的AI服务之间,隔着多少工程决策。

很多朋友的路径是两个极端:要么从Transformer原理开始啃,读了一堆论文,最后连一个能跑的服务都写不出来;要么只学怎么调API,拿LangChain模板拼个Demo,一旦遇到生产环境的问题就抓瞎。我这次刻意选了"从零"这条更麻烦的路,不是为了情怀,而是想确认自己每一步到底在做什么、为什么这么做。

1. 为什么"从零开始"反而是AI工程最被低估的一步

先聊一个现象:很多开发者拿到大模型API后的第一反应,是去搜现成的框架和模板,然后往里面填业务逻辑。这没有错,但如果你不知道框架在替你解决什么问题,出了问题你会非常被动。我见过不止一个团队,Prompt效果不好就往LangChain里加索引、加记忆、加向量库,最后系统慢得离谱,谁也不知道是哪一层拖了后腿。

所以这次做"ai-engineering-from-scratch",我给自己定了一条规矩:每一步都要先问"为什么"。为什么需要框架?为什么需要Agent?为什么需要向量检索?如果答案不清晰,就先自己实现一个最简版本。这个过程中,最大的收获反而不是代码水平提升了,而是把AI应用的运行逻辑彻底看清楚了。

一条典型的AI工程链路长这样:

  1. 用户输入进入应用层。
  2. 应用层做预处理(清洗、格式化、判断意图)。
  3. 构造Prompt,带上必要的上下文和工具定义。
  4. 调用模型API(或本地推理服务)。
  5. 拿到模型输出,做解析和后处理(从文本里抽JSON、去噪、校验格式)。
  6. 把结构化结果交给下一层业务逻辑或工具执行。
  7. 把执行结果反馈给用户,并且保存对话记录。

如果你直接上框架,框架会帮你把2到7的很多细节封装掉。你写的代码少了,但你对手里系统的控制力也弱了。举个例子:Token的消耗、上下文的长度、模型输出的随机性,这些在框架里都是"配置项",但真正出问题时,你对它们的理解决定了你排查效率的高低。

从零开始的另一个好处,是你能把"最小闭环"做扎实。所谓最小闭环,就是不用任何封装,直接完成一次完整的模型调用:构造Prompt,拿到返回值,解析结果。我第一次做的时候,只是让模型做了一道简单的代码解释题,然后把输出原样打印出来。看似无聊,但这一步跑通之后,后面所有复杂功能才有了可靠的底座。

我在这个阶段排除掉的第一个错误认识是:"Prompt Engineering"是文科活,随便写写就行。实际上,Prompt的质量直接决定了整个工程的下限,而它本身是可以用工程手段去管理、迭代、评测的。这个在后面会专门展开。

2. 从零搭建AI工程的完整技术栈选型

技术栈选型是整个项目中我犹豫最久的部分。因为大模型生态变化太快,今天选的东西明天可能就过时了。所以我的原则是:选稳定、简单、贴近底层的东西,让框架成为可替换的薄层,而非不可动摇的地基。

2.1 模型接入方式的取舍:API调用还是本地部署

先给结论:初学者和大多数业务场景,优先用云端API;只有当你有数据合规要求、离线需求、或者Token成本已经高到不可忽略时,才考虑本地部署开源模型。

我做了一张对比表,方便你直观感受两条路的差异:

对比维度云端API(如GPT系列、Claude、国产闭源模型)本地部署开源模型(如Qwen、DeepSeek等)
上手门槛低,注册拿Key就能调高,要处理推理框架、显存、依赖
效果上限通常更高,通用能力强取决于模型尺寸和微调水平
成本按Token计费,长期高频使用贵一次性硬件投入大,边际成本低
数据安全依赖服务商政策,敏感数据要谨慎数据不出内网,可控性强
运维复杂度无需关心推理服务需要监控显存、并发、模型版本
可控性Prompt和接口参数可控采样参数、量化、系统提示都可自定义

我的建议是一个"混合理念":业务上线初期用云端API验证需求,积累足够调用量之后,再评估哪些高频路径值得切到本地模型。不要一开始就在硬件和部署上投入太多,AI项目的失败风险更多来自需求判断错误,而不是模型能力不足。

2.2 基础设施:环境隔离和依赖管理是保命线

从零开始做AI工程,最容易翻车的地方反而是最基础的地方:Python环境、依赖版本、配置文件。

我的本机常年有其他项目,所以我给这个项目单独建了一个虚拟环境。如果你用Conda,可以这么做:

conda create -n ai-engine python=3.11 conda activate ai-engine pip install openai pydantic python-dotenv langchain-core

这里多说一句为什么要用3.11:现在主流模型SDK对3.11的兼容性最好,3.12和3.13在某些pydantic、pydantic-core相关的依赖上容易遇到编译问题。别在这上面浪费时间。

所有密钥和环境变量,统一放到.env文件里,用python-dotenv加载,代码里绝不出现明文Key。配置文件则是越简单越好,我初期只有三个配置项:模型名称、Temperature、最大Token数。后来增加的配置项(比如评测数据路径、日志级别)也都是按需添加的,而不是一开始就搞一个几十项的复杂配置文件。

3. 提示工程与Agent编排:从"能聊"到"能用"

如果说前面的工作是在搭骨架,那这一章就是给系统注入灵魂。我这次的核心应用是一个内部技术问答助手,它要做的不只是"聊天",而是回答问题时能查代码仓库、能读文档、能执行简单的查询命令。这就完全跨进了Agent的领域。

3.1 Prompt模板化:别让提示词在代码里裸奔

很多人第一次见Prompt工程时,以为就是在代码里拼一个超长字符串。这种"裸奔式"写法有三大问题:难以维护、难以评测、难以在不同场景复用。

我采用的做法是"结构化Prompt模板"。下面是一个实际用过的模板结构:

[系统角色] 你是{role},你擅长{expertise}。 [任务目标] 请根据以下内容完成{task}。 [约束条件] 1. 只基于给定的上下文回答,不要编造信息。 2. 如果信息不足,请明确回答"信息不足"。 3. 输出格式必须为Markdown。 [上下文信息] {documents} [对话历史] {chat_history} [用户当前问题] {question}

你注意看,模板里全是占位符,真正的变量在调用时才填入。这样做的价值是:你可以在一个地方统一管理所有Prompt,改一句话,所有调用点同步生效。我把所有Prompt模板放在一个独立的目录下,命名为prompts/,每个场景一个文件,类似代码里的模块化管理。

另外,我强烈建议在模板里显式写出"约束条件"。很多时候模型返回的结果不是"不对",而是"格式不规范"或"超出范围"。这些约束写在Prompt里,能减少大量后处理代码。

3.2 上下文管理:让模型看到它该看到的东西

上下文(Context)是AI工程里最贵的资源之一。它既影响效果,也影响成本。模型输入是按Token计费的,你塞进去的每一段不相关文字都在花钱,还会稀释模型对重点信息的注意力。

这个项目的上下文来源有三个:对话历史、检索到的文档片段、用户当前输入。怎么管理它们?

第一,对话历史不能全量塞。模型窗口有限,早期对话可能已经在无关紧要的内容上绕了很久。我的做法是:保留最近N轮对话,N由"业务需要的记忆长度"和"Token预算"共同决定,同时把更早的消息压缩成一段摘要。这个"摘要压缩"可以在新对话开启时让模型生成一次,也可以定期做。

第二,检索文档要按相关性截断。单纯把整篇文档塞进Prompt,不仅浪费Token,还容易引入噪音。我有一次用了一个简单的关键词匹配来选文档片段,结果效果远差于手动选择的相关段落。后来我把检索逻辑换成"先粗筛再重排",先用快速方法从上万条记录里筛出候选,然后再用模型或嵌入相似度做精细排序,效果立刻提升了。

3.3 Agent编排:从顺序调用到"工具循环"

网上说Agent的文章很多,但真正落到工程上,核心就一句话:Agent是一个循环——模型决定调用什么工具、传入什么参数、拿到工具结果后再决定下一步。这个循环最原始的实现其实并不复杂:

from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) # 工具定义,对应模型function calling(现在叫tool calling)能力 tools = [ { "type": "function", "function": { "name": "search_code", "description": "在代码仓库中搜索关键词相关的代码片段", "parameters": { "type": "object", "properties": { "query": {"type": "string"}, "file_pattern": {"type": "string", "default": "*.py"} }, "required": ["query"] } } } ] def call_agent(user_input, messages): messages.append({"role": "user", "content": user_input}) # 第一步:让模型决定是否需要调用工具 response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, tool_choice="auto", ) msg = response.choices[0].message # 如果模型要求调用工具 while msg.tool_calls: # 将模型回复加入消息历史 messages.append(msg) # 执行工具并获取结果 for tool_call in msg.tool_calls: if tool_call.function.name == "search_code": result = search_code(**json.loads(tool_call.function.arguments)) else: result = "未知工具" messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) # 把工具结果送回模型,让它继续 response = client.chat.completions.create( model="gpt-4o",messages=messages, tools=tools, ) msg = response.choices[0].message # 没有tool_calls,说明模型给出了最终回答 messages.append(msg) return msg.content, messages

这个循环看起来简单,但里面藏了三个非常关键的工程决策。

第一个是工具返回结果的序列化。工具(比如搜索代码、读文件)返回的是结构化数据,但在传给模型之前必须转成文本。这里有个取舍:结构越细,模型越容易读懂,但Token消耗越大;结构越粗,Token越省,但模型可能误解。我最后的经验是:优先保证"关键信息准确且聚焦",把不重要的字段过滤掉再传给模型。

第二个是最大迭代次数控制。真实的Agent不能无限循环下去,否则模型可能反复调用同一个工具,浪费Token而且用户等待时间不可接受。我给循环加了一个上限,比如最多5轮工具调用,达到上限后强制让模型基于已有信息给出回答,并且在回答中说明"部分信息未能获取"。

第三个是"工具调用结果"本身的记录。这个在调试时极其重要。我会把所有工具调用信息落日志,包括工具名、传入参数、结果摘要、耗时。这样一旦用户反馈某个回答不对劲,我能通过日志还原出模型当时"看到了什么",从而判断是检索问题、Prompt问题还是模型决策问题。

4. 评测、上线与迭代:让AI工程真正"可控"

很多人做一个AI Demo、让几个朋友试用一下,觉得效果不错,就直接上线。这种做法风险极大。因为大模型的输出是概率性的,你看到的"不错"可能只是在少数样例上"不错",换一批输入可能完全拉胯。AI工程和传统软件最大的区别就在这里:传统软件的逻辑是确定的,AI的行为是分布式的。所以AI工程必须有一套自己的"评测-迭代"闭环。

4.1 评测集:没有数据,就别谈优化

我做的第一件事是收集和整理评测数据。让团队成员准备了30个典型问题,覆盖这几类:代码查询、概念解释、文档指引、边界问题(比如找不到答案时应该怎么办)。每一个问题都配了参考答案和评分标准,比如"答案是否准确""格式是否规范""是否有包含无关内容"。

这在很多人看来是浪费时间,但实际效果立竿见影。我第一次跑评测时,发现助手在回答"这个仓库里有没有处理超时的工具函数"时,会给出一个"有",并且引用了不相关的日志代码片段。这属于典型的"幻觉"。如果没有评测集,这类问题可能要到生产环境被用户骂了才会暴露。

4.2 自动化评测脚本:省下你的命

人工评测几轮可以,但每次改Prompt、换模型参数都要全部人工来一遍,不现实。我的做法很简单:写一个脚本,把评测集里的每个问题按顺序发给系统,然后把输出保存为JSON,先自动做两件最基础的事。

一是格式校验:输出是否包含特定字段,比如是否返回了Markdown代码块。二是关键词命中:答案中是否包含预期出现的关键实体(比如某个函数名、文件名)。

def evaluate(prompt, reference_keywords): output = call_agent(prompt) result = { "prompt": prompt, "output": output, "length": len(output), "contains_keyword": all(kw in output for kw in reference_keywords), "has_code_block": "```" in output, } return result for item in eval_set: result = evaluate(item["question"], item["expected_keywords"]) results.append(result) # 汇总通过率 pass_rate = sum(r["contains_keyword"] for r in results) / len(results)

自动评测不能替代人,但它能帮你快速筛掉"明显有问题"的版本,让有限的精力和时间聚焦在真正值得人工判断的少数输出上面。这类脚本十几分钟就能写完,但能省下你后面几十个小时的暗耗。

4.3 可观测性:日志和监控是AI应用的生命线

AI应用上线后,最怕的是"静默失败":用户问了一个问题,模型返回了一个看起来还算通顺、但事实完全错误的答案。用户可能就当这个产品不好用,转手就走,而你根本不知道自己的系统里发生了什么。

所以从第一天起,我就在每个请求的关键节点打日志,包括:用户输入、最终Prompt拼装结果、模型原始输出、解析后的结构化结果、耗时和Token数。后来我加了一个分类:每一条日志都会标记"本次请求是否有工具调用、调了哪个工具、工具结果长什么样"。这样我在定位问题时,能迅速缩小范围。

比如有一次用户反馈"助手回答问题时总是不使用工具,导致答案过时"。我查了日志,发现问题出在Prompt里有一句"如果文档里已有信息就直接回答",模型经常简化决策,跳过了工具调用。我只调整了一处Prompt引导语的措辞,从"如果文档里已有信息就直接回答"改为"先检索必要的文档,再基于检索结果回答",效果立刻就变了。这正是工程化之后才能做的精准修正。

4.4 迭代节奏:先稳定,再优化

我自己总结出的迭代节奏是:先跑通全链路、记录基线,然后再做第一轮Prompt优化。很多人一上来就改Prompt调参数,但你连当前系统"基准表现"是多少都不知道,改完到底有没有进步也说不出来,那就谈不上迭代了。

我的做法是:

  1. 第一版用最原始的Prompt运行评测,记录分数和问题类型,形成基线。
  2. 针对评测暴露的"幻觉多"或"格式乱",逐类修改Prompt模板,每次只改一处,重新运行评测。
  3. 如果某类问题持续存在,考虑调整检索逻辑或工具定义,而不是继续堆Prompt。

这个节奏听起来慢,但收益非常稳定。每次改动都有数字背书,避免了一拍脑袋式的"优化"。

5. 从零实践中最容易踩的坑与我的取舍

最后这部分,我把自己在这次从零实践中踩过的、以及身边朋友栽过的坑集中复盘一下。每一个坑背后都代表一类工程认知,值得单独拿出来讲。

5.1 实例污染:你以为模型懂了,其实它在模仿

我第一次给问答助手写Prompt时,示例里放了一个"完美的回答范文"。结果评测的时候,我发现无论用户问什么,模型的输出风格都往那个范文上靠,甚至偶尔会把范文里的内容当成真实结果输出。这是典型的"少样本示例污染"。

后来我把示例数量从3个减到1个,且示例的内容与高频真实问题尽量不相关,同时明确加上"上面示例只是格式参考,不是事实来源"的声明。这个问题就基本消失了。你可以把"示例"想象成给孩子看的拼音作业本——它管的是书写格式,绝不是考试答案的来源。

5.2 上下文漂移:对话轮数越多,结果越差

很多AI应用都有这个问题:用户和模型对话很久之后,模型开始"忘记"关键约束,或者被用户引导到错误的前提上。我的处理方式是把"关键约束"在每次构造Prompt时重复注入,而不是只放在最初的系统消息里。这样即使发生在第20轮对话,模型仍然记得"不要编造信息"。

另外一个做法是"定期收束记忆":当对话超过N轮或Token接近阈值时,让模型生成一段"到目前为止已确认的事实"的摘要,然后清空原始对话历史,保住对话的焦点。

5.3 模型随机性:同样的输入两次结果不一样是正常的

这是AI工程最需要被正视的一点。传统软件测试里"同样的输入必须得到同样的输出",在AI项目里不成立,模型有采样参数(Temperature),结果天生就有波动。

我为应对这一点做了两件事。一是在评测时,同一个问题跑多次(比如3次),取多次结果的"一致通过率",而不是单次结果;二是生产环境的对话里把Temperature调得较低(比如0.2),换取稳定性。这两件事牺牲了一点点随机性和创造力,但对大多数工具类AI应用来说,稳定性远比文采重要。

5.4 成本失控:别把Token当水喝

有一次我测试一个长文档问答功能,把一篇2万字的文档全塞进了Prompt。模型回答质量确实高了,但单次调用的Token成本翻了十几倍,而且响应时间变得很长。我后来把策略改成"先做文档切块,只检索与问题最相关的一两个块",Token消耗立刻降下来了,响应时间也缩短到原来的三分之一,回答质量并没有明显下降。

AI工程里有个词叫"Token经济学",意思是模型调用的一切设计都要考虑Token成本与效果之间的平衡。上下文按需取、历史按摘要存、工具返回只保留关键字段,这三件事能同时改善成本、速度和效果,是我这次实践里觉得最值得坚持的优化方向。

6. 一些关于未来的延伸思考

这次的"ai-engineering-from-scratch"项目,我刻意选择了从最原始的工具组合做起,期间积累的东西让我在之后用起任何框架时都更有底气。现在看到LangChain、CrewAI、AutoGen这类工具,我不会觉得"看不懂",而是能把它们的功能一一映射到我亲手搭建过的模块上,真正理解它们在替我做哪些工作、又可能掩盖哪些问题。

未来我打算在几个方向上继续扩展:

第一个方向是评测的立体化。目前我的评测还是以关键词、格式等浅层指标为主,下一步打算引入模型即评委(LLM-as-a-judge)的方式做语义质量打分,同时建立人工评测的抽样流程,形成"机器初筛+人工抽检"的双层机制。

第二个方向是记忆体系的模块化。把长期记忆、短期记忆、场景记忆彻底分层,让Agent既能记住用户偏好,又不会因为记忆太多而影响本次回答的准确性。这中间的读写策略,我计划继续用"从零"的方式自己实现一版,再与成熟方案对比。

第三个方向是"评测集的反向构造"。除了收集真实用户问题,我还会根据系统能力边界构造"极端问题":超长输入、自相矛盾的问题、故意诱导模型说错的Prompt注入。这类对抗样本能大幅提升系统的鲁棒性,也是AI工程化水平从"能用"走向"好用"的关键一步。

这次实践给我的最大体会是:AI工程并不玄学,它就是把"模型调用"这件事从"实验脚本"变成"可靠服务"的过程。这个过程中没有银弹,没有一键部署就万事大吉的工具,有的只是对每一步的追问——为什么这么构造Prompt、为什么选这个模型参数、为什么检索看到这块内容。当你能回答清楚这些"为什么",从零到一就不再艰难,反而踏实。

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

豆包高阶使用技巧:提升社交效率与商业价值

1. 豆包使用场景全解析 豆包作为一款国民级社交应用插件,已经深度融入我们的日常生活。从学生党到上班族,从宝妈群体到退休人士,几乎每个智能手机用户都会在某个时刻打开这个绿色图标的应用。但真正能把豆包功能用到极致的用户却不足20%&…

作者头像 李华
网站建设 2026/11/4 10:25:48

多视图二分图聚类的概率图模型解析与应用

1. 论文核心思想解析 TPAMI-2025发表的《Generalized Probabilistic Graphical Modeling for Multi-View Bipartite Graph Clustering》提出了一种创新的概率图模型框架,专门针对多视图二分图聚类问题。这项工作的核心突破在于将传统的概率图模型扩展到了多视图异构…

作者头像 李华
网站建设 2026/11/4 10:24:07

从零实现AI工程:手写神经网络与反向传播核心指南

“ai-engineering-from-scratch”这种命名方式,我太熟了。GitHub上一搜一大把,几乎每个热门方向都会冒出来几个“from scratch”仓库,但真正能让人从头走到尾、不半路劝退的,少之又少。这个标题的核心诱惑力在于“我什么都不要&am…

作者头像 李华
网站建设 2026/11/4 10:23:11

结构优化仿真在工业设备中的应用与实践

1. 项目背景与核心价值结构优化仿真在工程领域就像给建筑做全面体检的CT扫描仪。我们团队最近完成的"主题052"项目,就是针对某型工业设备关键部件的系统性强度优化工程。这个项目最特别之处在于,它不仅仅是一次常规的仿真分析,而是…

作者头像 李华
网站建设 2026/11/4 10:21:51

OpenStack Glance服务迁移实践指南

1. OpenStack Glance服务迁移概述 在OpenStack云平台的实际运维中,随着业务规模扩大,将核心服务从控制节点分离出来独立部署是常见的架构优化手段。最近我就完成了将Glance镜像服务从控制节点迁移到独立主机的完整过程。这种迁移不仅能减轻控制节点负载&…

作者头像 李华
网站建设 2026/11/4 10:21:17

C语言停车场管理系统:栈队列建模与课设代码避坑

简介:面向高校软件工程课程设计与项目答辩场景的停车场管理系统报告书,围绕车辆停放管理需求,完整呈现需求分析、概要设计、详细设计、编码实现与测试的分析过程,可帮助读者理解软件工程方法如何应用到小型管理系统中。系统以 Win…

作者头像 李华