
最近折腾了几个 AI 项目,踩了不少坑,这篇把从 LLM API 到 Agent、Harness 再到生产系统的东西捋清楚。
把大模型接进应用里其实不难,发个请求、收个响应、显示出来就完事了。但要做成一个可靠的应用,那就是另一回事了。
真实的应用需要管理上下文、执行工具、保持状态、处理中断、请求审批、处理异常、恢复故障,还得把进度反馈给用户。有些应用还要处理图片、音频、视频、文档、结构化数据或者实时事件。
需求一多,模型就成了整个系统里的一个组件,而不是全部。
这篇文章聊一聊 AI 应用工程的完整图景:从模型能力、SDK 集成,到 Agent 循环、上下文管理、Harness 工程,再到生产级运维。
会拿 Vercel AI SDK 写点 TypeScript 示例,其他厂商的模型(OpenAI、Anthropic 等)思路也是相通的。
目标不是把 API 清单背一遍,是搞清楚这些组件怎么配合、各自解决什么问题、职责边界在哪里。
先建个 mental model。
一个生产级的 AI 应用可以理解成几层协作的结构。
模型提供的能力包括:
不是每个模型都支持所有能力,可用性、质量、限制和定价取决于具体的模型和提供商。
厂商 API 通过网络接口暴露模型能力。SDK 则在这个接口上封装了一层,提供类型化的函数、请求构造器、流式响应辅助工具和厂商特定 abstractions。
运行时控制模型周围发生的事情:
用户界面把运行时的行为传达给用户:
生产系统还需要:
一个有用的原则是:模型提议,运行时协调,工具执行工作,应用强制执行策略。
这个分离在 AI 应用越来越自主化的趋势下会变得越来越重要。
不需要把 Transformer 架构的每个细节都搞懂,但得理解那些影响系统设计的概念。
模型根据输入和它学到的参数生成输出。在应用里,把模型跑起来产生输出的过程叫推理(Inference)。
输入可能包含指令、对话消息、检索到的文档、图片、音频、工具定义或者结构化数据。
输出可能是纯文本、结构化内容、语音回复,或者是调用工具的请求。
模型不会自动访问你的数据库、文件系统、内部 API 或者业务系统。这些能力必须通过合适的集成来暴露。
模型通过底层表示来处理文本和其他内容。对于文本,通常用 Token 而不是词或字符来衡量。
Token 影响:
上下文窗口是模型一次请求能处理的最大上下文量。具体限制和计费规则因模型而异。
更大的上下文窗口不自动意味着更好的效果。不相关或者矛盾的信息可能让请求效果变差,即使它还能塞进容量里。
很多对话 API 把交互表示为带角色的消息。
常见角色包括:
具体消息结构在不同厂商间有差异。有些 API 还支持更丰富的内容块来处理图片、音频、推理相关数据和工具交互。
对话不一定只是一串纯文本消息的数组,它可以是不同类型内容和事件的序列。
有些模型支持推理导向的能力或可配置的思考模式,能在复杂任务上提升表现。
这些对编码、规划、分析和多用步骤工具调用很有用。但推理能力在不同模型和厂商间差异很大。
应用开发者应该区分:
不要以为暴露内部推理过程是必须的。一个好用的应用可以展示简洁的解释、来源、行动历史和可验证的结果。
推理也有实际成本:更多的计算通常意味着更高的延迟和 Token 消耗。
最重要的一点认知是:AI 不等于在聊天窗口里生成文字。
现代 AI API 暴露了多种能力,一个应用可以在一个工作流里组合使用它们。
文本生成是我们熟悉的聊天助手、摘要、改写、翻译、分类、内容生成背后的能力。
典型场景:
输出可以流式输出,也可以等生成完再返回。
有时候应用需要的是可预测的数据结构,而不是自由格式的文本。
比如一个任务分类器可能需要返回:
{ "category": "billing", "priority": "high", "requiresHumanReview": true
}
结构化输出功能让开发者用 Schema 或支持的结构化生成机制来描述期望的格式。
这对以下场景很有用:
Schema 约束的输出能减少格式错误,但不保证值是正确的或操作是安全的。要独立验证结果并强制执行业务规则。
有视觉能力的模型可以分析通过支持 API 传入的图片。
可能的应用场景:
图片支持在格式、分辨率、细节处理和 Token 计费上各有差异。
文档理解可能还需要 OCR、布局分析、PDF 解析或专门的文档处理管道。模型能理解一张图片不意味着它能完美提取每个字符或表格。
音频能力可以支持:
一个简单的语音助手可能用这样的管道:
其他架构用能同时处理多个阶段的语音模型。
这些设计在延迟、成本和交互特性上各有不同。实时语音应用可能还需要打断处理、轮次检测、流式音频和会话管理。
视频理解可以通过支持的视频输入、采样帧、音频轨道或模型特定的视频接口来构建。
可能的应用场景:
开发者需要考虑帧采样、时间戳、音频同步、数据量和处理成本。模型收到的是选定的帧,不一定理解视频的每个瞬间。
嵌入向量把内容表示成可以数学比较的数值向量。
它们对以下场景有用:
比如两篇文档讨论同一个概念但用了不同的词汇,语义搜索系统仍然能检索到相关内容,即使查询没有共享精确的关键词。
嵌入向量不是答案本身。它们是被检索和相似度系统使用的表示。
模型可以请求暴露以下能力的工具:
模型通常提出工具调用并提供参数。应用或提供商管理的执行环境决定实际运行什么。
这个区别很关键:工具调用不仅仅是生成的文本,描述一个动作不等于执行它。
有些系统支持计算机使用或浏览器使用能力,让 Agent 能通过截图、点击、输入或其他操作与支持的界面交互。
这些能力可以实现:
计算机使用引入了额外的风险。Agent 可能误解屏幕内容、点错控件或遇到不受信任的内容。敏感操作需要强健的执行边界、验证和适当的人工监督。
如果存在稳定的 API,通常比通过 UI 控制同一系统更可靠。
按职责归属来分类功能很有帮助。
| 能力 | 主要职责 |
|---|---|
| 文本、视觉、音频、支持的视频 | 模型和提供商 |
| 结构化生成 | 模型/API,应用需验证 |
| 嵌入向量 | 嵌入模型和检索基础设施 |
| 流式响应 | API 传输和 SDK |
| 工具选择 | 模型引导行为,取决于 API |
| 工具执行 | 应用运行时或提供商管理的执行 |
| 记忆 | 应用架构和存储 |
| RAG | 检索管道和应用逻辑 |
| 审批 | 应用授权和用户体验 |
| Hooks 和中间件 | 运行时或框架生命周期 |
| 压缩 | 上下文管理策略 |
| UI 渲染 | 前端应用 |
| 评估和追踪 | 应用运维和评估系统 |
不是每个提供商都支持每个功能,而且名称相似的功能可能有不同的语义。
这两个术语紧密相关,但解决的问题不同。
Prompt 工程关注的是给模型的指令和框架。
一个 Prompt 可能定义:
比如:
You are a support assistant. Classify the incoming request into one of:
- billing
- technical_support
- account_access Return only the requested structured result.
If the information is insufficient, mark the result as uncertain.
这告诉模型怎么处理这个任务。
上下文工程关注的是在特定时刻选择和组织哪些信息供模型使用。
可能包括:
核心问题不只是"应该告诉模型什么",还有"对于这个具体决策,什么信息应该是可用的"。
模型可能有很好的指令,但如果收到过期数据、不相关文档、缺失的权限或矛盾的对话历史,仍然可能产生糟糕的结果。
实际应用通常在推理前有一个上下文构建阶段。
比如:
Incoming request | v
Identify task and permissions | v
Load relevant session state | v
Retrieve relevant information | v
Select available tools | v
Assemble model context | v
Run inference
这个过程应该是审慎的。默认把每个可用的文档、工具定义和之前的消息都塞给每次模型调用,通常不是好做法。
上下文污染发生在不相关、冗余、过时或矛盾的信息消耗了注意力或容量时。
常见原因包括:
更多上下文不一定更好。目标是提供对当前任务最有用的上下文。
Prompt 缓存让支持的提供商能复用符合条件的重复 Prompt 前缀的处理。
在合适的工作负载中能降低延迟和输入处理成本,尤其是稳定的指令或工具定义被复用时。
但 Prompt 缓存不等同于对话记忆或上下文压缩:
这些机制可以互补。
SDK 简化了集成,但不等于不需要理解底层模型和 API 行为。
特定于提供商的 SDK 通常暴露特定提供商的能力和约定。
它们可能提供对提供商特定请求参数、响应格式、流式事件和专业能力的直接访问。
当应用需要精确控制某个提供商的 API 时很有用。
统一的 SDK 可以为多个提供商提供通用接口。
比如 Vercel AI SDK 提供了文本生成、流式响应、工具调用、结构化输出、嵌入向量和其他支持的 AI 任务的函数。
一个简单的 TypeScript 示例:
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai"; const result = await generateText({ model: openai("gpt-4.1"), prompt: "Explain context engineering in simple terms.",
}); console.log(result.text);
这展示了非流式文本生成的请求。模型标识符是示例,选择你账户可用的、支持所需操作的模型。
SDK 提供了方便的接口,但你的应用仍然需要处理认证、错误、超时、成本控制和合适的数据处理。
对于交互式应用,等整个响应返回才显示体验很差。
流式响应让应用能增量消费输出。
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai"; const result = streamText({ model: openai("gpt-4.1"), prompt: "Explain how an AI agent uses tools.",
}); for await (const chunk of result.textStream) { process.stdout.write(chunk);
}
这个示例把文本流式输出到服务器端。Web 应用通常会用合适的响应格式把流发送到前端。
流式响应能改善感知到的响应速度,但不一定会减少产生最终响应所需的总计算量。
另外,不是每个流式事件都是面向用户的文本。工具调用、结构化数据、推理相关内容、状态事件可能需要不同的处理。
两个 SDK 可能暴露名称相似但语义不同的概念。
比如:
把 SDK abstractions 当作实现接口,而不是证明每个提供商行为相同的证据。
工具调用是 AI 应用工程中最重要的概念之一。
工具暴露一个定义好的操作,模型可以请求这个操作。应用决定如何验证、授权和执行这个请求。
一个工具通常包含:
比如,一个客服助手可能有一个 get_order_status 工具。
模型可能请求:
{ "orderId": "ORD-12345"
}
应用验证参数、检查用户是否可以访问那个订单、查询相应的服务、返回结果。
模型然后可以用这个结果来组织答案。
这是分开的阶段:
有些 API 和 SDK 把这个过程的部分自动化了。其他的把这些留给应用处理。
关键点是应用必须知道哪个组件负责执行。
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod"; const result = await generateText({ model: openai("gpt-4.1"), prompt: "Find the status of order ORD-12345.", tools: { getOrderStatus: tool({ description: "Retrieve the status of an order.", inputSchema: z.object({ orderId: z.string(), }), execute: async ({ orderId }) => { // Authenticate and authorize the request in a real application. // Query the order service here. return { orderId, status: "shipped", }; }, }), },
}); console.log(result.text);
这是说明性的集成模式,不是完整的生产订单服务。示例处理器返回静态结果。真实实现需要验证访问、处理服务错误,并且在没有授权检查的情况下不要盲目信任模型提供的订单标识符。
随着工具数量增长,决定暴露哪些工具就成了架构问题。
可能的策略:
比如,一个开发者助手可能有访问 GitHub、工单跟踪、内部文档和部署系统的权限。关于 PR 的问题不一定需要加载所有部署和文档工具。
动态发现能减少上下文使用,但发现机制本身需要授权和可观测性。
有些任务通过并发运行独立操作能完成得更快。
比如,助手可能同时获取文档和工单元数据。
并行执行需要注意:
独立的读操作通常是并行的好候选。有冲突的写操作或有依赖的操作需要更仔细的协调。
仅靠工具调用不能让一个应用成为完整的 Agent 系统。
一个 Agent 工作流通常涉及重复的决策和操作,直到达到目标或系统确定无法继续。
简化的循环看起来是这样的:
Receive goal | v
Assemble context | v
Ask model for next step | v
Tool call requested? / \ No Yes
| |
v v
Finish Validate and authorize | v Execute tool | v Record result | +------> Continue loop
循环必须有终止条件。否则 Agent 可能重复调用工具、消耗资源或不取得进展。
有用的控制包括:
工作流遵循预定义的步骤序列。
比如:
Receive support request | v
Classify request | v
Retrieve account information | v
Draft response | v
Human review | v
Send response
Agent 在选择下一步时有更大的灵活性,基于当前上下文和之前操作的结果。
工作流适用于流程事先已知的情况。Agent 适用于路径取决于执行中发现的信息的情况。
很多生产系统把两者结合:确定性工作流建立边界,Agent 处理选定的推理和工具使用步骤。
编排协调多个步骤或组件的执行。
它可能处理:
编排不等于多 Agent 架构。单个 Agent 可以通过复杂的工作流进行编排,多个 Agent 也可以在相对简单的编排系统内运行。
多 Agent 系统把职责分配给多个模型驱动的组件。
比如:
当任务有不同上下文、能力或职责时这很有帮助。
然而,多个 Agent 也引入了协调开销、更多推理成本、更多故障模式和可能的分歧。应该在专业化带来可衡量优势时才使用,而不是仅仅因为架构允许。
Hooks 和中间件在 AI 工程中经常被提到,但含义因框架而异。
一个有用的理解方式是考虑 AI 请求的生命周期。
Hook 让运行时或应用能把自定义行为附加到定义的 lifecycle 事件上。
根据框架,相关事件可能包括:
Hooks 可用于日志、验证、指标、策略检查和其他横切行为。
具体的 API 和执行语义是框架特定的。AI SDK 里的 hook、编排框架里的生命周期回调和编码 Agent 运行时里的 hook 不能自动互换。
中间件包装或拦截操作以应用可复用的行为。
例子包括:
当相同行为必须一致地应用于许多请求时,中间件通常很有用。
工具是模型可以请求的操作。
Hook 是与应用或运行时事件关联的逻辑。
比如:
searchDocumentation 是一个工具。Hook 可能调用应用逻辑,但不应该与模型选择的工具混淆。
生命周期回调只有在可靠地在相关执行边界运行且不能被其他路径绕过时才对安全有用。
对于敏感操作,授权应该由实际的服务或执行层强制执行。UI 限制或 Prompt 指令本身是不够的。
自主性不总是正确的行为。
有些操作应该要求明确的用户授权,有些任务在应用获得额外信息前无法继续。
审批在提议的操作继续前引入一个决策点。
例子包括:
安全的执行流程:
Model proposes action | v
Runtime validates arguments | v
Policy checks required
authorization | v
Approval required? / \ No Yes
| |
| v | Pause execution
| |
| v | Present details
| |
| v | User approves? | / \ | No Yes
| | |
v v v Execute or reject action
审批不仅仅是 Prompt 指令让模型小心。
运行时必须实际阻塞执行,直到收到合适的授权。
审批记录理想情况下应该标识提议的操作、参数、相关用户和决策结果。如果审批后操作有变更,系统应该再次验证。
中断暂停工作流,因为需要外部的东西才能继续。
原因可能是:
中断应该保留足够的状态以便安全恢复,而不是强制整个任务重新开始。
有时候模型需要在做有用决策前澄清。
比如用户可能说:
安排下周和团队的会议。
应用可能需要知道是哪个团队、哪一天、会议持续多久。
好的系统应该只问继续所需的问题。还应该保留答案,这样用户不需要重复。
澄清和审批是不同的: