2026 前端 AI Agent 工程化实战营系列第 2 篇
本文整理自《2026 前端 AI Agent 工程化实战营》课程资料,作为系列文章第 2 篇。

如果说上一章讨论的是开发范式为何会改变,那么从这一章开始,我们将正式进入实践层面:一款智能体究竟应该如何着手开发。
在真正动手之前,有一个问题必须先想清楚:当我们谈“开发智能体”时,究竟是在开发什么?是一个会聊天的模型、一个能执行任务的流程,还是一种能够接入系统并持续运行的工程化能力?
要回答这个问题,最合适的方式不是一上来就进入复杂框架,而是先回到智能体最基础的一层:模型调用本身。因为后续所有能力——无论是工作流、工具调用,还是检索增强与多智能体协作——最终都建立在这一层之上。
因此,本章不会急着讨论复杂的 Agent 设计,而是先解决几个更基础的问题:如何定义角色与任务,如何组织上下文,如何约束输出,如何让模型结果被程序稳定接住,以及如何把一次调用逐步封装为可复用的能力模块。
注:本书示例均基于 Node.js 技术栈,且示例所用的所有接口均由超哥的 mux 友情赞助。
1.1 起点:为什么要从模型调用开始
很多人在初学智能体开发时,会下意识把注意力放在更显眼的部分:工具调用、任务编排、多轮执行、自动检索,甚至多智能体分工。它们当然重要,但它们不是起点,而是建立在更底层能力之上的结果。
真正的问题在于:如果模型输入设计不清晰、输出结果不稳定、返回格式不可控,那么后续叠加的每一层能力,都只是在放大这种不确定性。工作流会放大错误,工具链会扩散偏差,自动化执行会把原本局部的问题直接带进整个系统。
这也是智能体开发和普通“用一下模型”之间最大的区别。前者关心的不是模型能不能偶尔给出一个不错的答案,而是它能否在系统里稳定工作。换句话说,我们需要的不是一次“看起来挺聪明”的生成,而是一种能够被约束、被复用、被验证的能力。
但大模型本身并不天然具备这种工程属性。它擅长生成文本、补全代码、组织语言、模拟推理;可如果这些能力没有经过设计与收束,就很难直接接入业务系统。开发者首先要做的,是把这种生成式能力整理为可工程化使用的形式。
从学习路径上看,这一步至少包含三个核心问题:模型调用的基本结构是什么、怎样降低输出漂移、怎样让结果能够被程序解析与消费。只有把这三件事处理清楚,模型才算真正从一个“会说话的黑盒”,变成能够进入系统的软件模块。
也正因为如此,模型调用并不是智能体开发之前的准备动作,而是智能体开发本身的起点。
1.2 调用:一次受约束的对话

当我们说“调用一次大模型”时,很多人脑海里的第一反应往往很简单:发一句话,然后等待模型返回一段回答。
如果只站在使用者的视角,这样理解并没有问题。日常对话式产品给人的体验也确实如此:你输入一句自然语言,模型返回一段看起来足够聪明的文本,整个过程像一次“提问—回答”的往返。
但一旦进入开发视角,事情就完全不同了。
对于开发者来说,一次模型调用并不只是“问一个问题”。它更像是在设计一次带有明确目标、角色边界、上下文信息和输出约束的受控交互。也就是说,表面上看你是在和模型对话,实际上是在构造一套输入条件,期待它在这些条件下稳定地产生某种结果。
从这个意义上讲,调用模型的过程,本质上并不是"把一句话交给 AI",而是在设计一次受约束的对话。
这也解释了为什么很多人在直接使用模型时会觉得“模型有时候很聪明,有时候又很不靠谱”;而真正进入工程实践之后,开发者会越来越清楚地意识到:模型输出质量的上限,往往取决于你如何组织这次调用,而不仅仅取决于模型本身有多强。
1.2.1 从“问答”到“受约束的对话”
如果把模型当作一个普通聊天对象,那么一次调用当然可以很随意。
你可以问一句“帮我分析一下这个需求”,也可以说“帮我写个函数”,甚至只给一个大概方向,让模型自由发挥。对于一些低风险、探索式、启发式的场景来说,这种方式并非完全不可用。
但智能体开发和普通聊天式使用最大的区别在于:系统不接受"差不多就行"。
在聊天场景里,一次回答偏一点、绕一点,甚至有些小问题,用户通常还能自行理解和纠正;可在工程场景里,一旦模型输出要被后续流程接住、要进入工具调用、要触发任务执行、要写入系统状态,“差不多”就不再是可接受的标准。这时,开发者关注的就不再只是“模型能不能答出来”,而是:
- 这次输入是否表达清楚?
- 模型会不会误解角色?
- 上下文是否足够?
- 输出格式是否可控?
- 结果能不能交给后续代码继续处理?
- 在相似输入下,结果会不会严重漂移?
所以,当模型进入软件系统之后,它就不再只是一个“很会回答问题的助手”,而是一个需要被设计、被约束、被验证的组件。
而这一切的起点,就是理解一次模型调用到底由什么构成。
1.2.2 模型调用的最小结构:输入、约束与输出
从最基础的角度看,一次完整的大模型调用通常至少包含几个核心要素:模型接收到的输入内容、被赋予的角色、当前任务的目标描述、相关的上下文信息、对输出形式的约束,以及模型最终返回的结果。这些要素看似普通,但几乎所有更复杂的智能体能力,最终都建立在它们之上。
如果换一种更直观的说法,一次模型调用大致是这样一个过程:先定义“它是谁”,再说明“它要做什么”,接着补齐“它需要知道什么”,最后明确“它应该怎样输出”。只有把这些环节组织清楚,模型输出才会从“偶尔有惊喜的自然语言生成”,逐步走向“可被系统稳定利用的能力”。
在最表层的理解里,模型输入就是你发给它的话,模型输出就是它回给你的话。但在工程语境下,“输入”和“输出”的定义要严格得多。
首先,输入并不只是“你问了什么”。很多开发者刚开始使用模型时,会把输入简单理解为一段 Prompt,但实际上,模型输入往往不止一句话,而是由多个部分共同组成的上下文集合。它可能包括一段 system 提示词,用来定义模型的角色和行为边界;一段 user 输入,用来描述当前任务;若干历史消息,用来维持上下文连续性;一段附加信息,比如业务背景、代码片段、文档内容;以及一些额外参数,比如温度、输出格式要求,甚至工具定义。
也就是说,模型真正“看到”的并不是某一句单独的话,而是一整个被组织过的上下文环境。这个理解非常重要,因为它会直接改变你设计调用的方式:如果你只把模型输入理解成“最后那一句用户提问”,就很容易忽略那些真正影响输出质量的因素——角色设定、任务边界、历史上下文、输出约束,恰恰都不在“最后一句话”里。
同样,输出也不只是“它回答了什么”。在工程实践里,输出至少有三层含义:
第一层,是它表达了什么内容:文本本身讲了什么,是否围绕任务,逻辑是否清楚。
第二层,是它是否按你要求的结构返回:比如你要求输出 JSON,它是否真的按 JSON 返回;你要求它返回字段列表,它是否漏字段;你要求只输出代码,它是否夹带了解释。
第三层,也是最关键的一层,是这段输出能不能被系统接住:也就是说,这个结果是否可以进入下一步处理,是否可以被程序解析,是否可以触发后续流程。
这一点非常关键。因为在智能体开发中,输出的价值不在于"看起来不错",而在于"是否可消费、可传递、可复用"。因此,从工程视角看,一次模型调用里的“输出”本质上并不是一段回答,而是一个将要进入系统后续流程的结果对象。
换句话说,模型调用更像一套“输入组织 + 约束设定 + 输出交付”的接口:你提供的不是一句话,而是一组被设计过的上下文;你写下的也不只是提示,而是让结果可控的规则;而模型返回的也不是终点,而是后续流程要消费的结果对象。
接下来要把这套接口真正落到可复用的结构里,第一步就是把信息分层清楚:哪些是长期稳定的规则,哪些是本次任务,哪些是需要保留的历史。对应到最常见的消息结构,就是 system / user / assistant 三个角色——它们分别承担什么职责?为什么会直接影响输出质量?
1.2.3 system/user/assistant 到底是什么
当我们把一次模型调用拆开来看,会发现它并不是把所有信息都塞进一段 Prompt 里那么简单。为了让模型更清楚地理解“什么是全局规则,什么是当前任务,什么是历史上下文”,现代大模型接口通常会把消息划分为不同角色。最常见的就是 system、user 和 assistant。
如果做一个最简化的理解,user 负责提出当前任务,assistant 负责承接历史响应并延续上下文,而 system 则负责定义整次交互的背景设定。它并不是在提问,而是在提前规定:模型应以什么身份、遵循什么原则、以什么行为方式参与当前任务。
也正因为如此,在这几个角色中,真正决定系统稳定性的往往不是 user,而是最容易被忽视的 system prompt。
很多人在刚开始使用模型时,习惯把 system 留空,把角色定义、任务要求、格式约束和背景补充全部揉进 user 里。短期看似乎也能工作,但一旦任务变复杂、调用变频繁、系统进入工程化阶段,这种写法很快就会暴露问题:Prompt 越写越长,职责边界越发模糊,模型行为也随之变得不稳定。
问题的根源在于:没有 system 作为稳定的控制层时,模型每一次调用都像是在“重新进入状态”。你需要反复告诉它“你是谁”“你应该怎么做”“哪些内容不要输出”“信息不足时如何处理”。这些内容如果每次都随着任务一起漂移,结果的一致性就很难保证。
而 system prompt 的价值,恰恰在于它为模型提供了一层相对稳定的全局控制。从工程角度看,它至少承担四个作用:
- 定义身份:模型以什么角色来理解任务,会直接影响表达方式、分析路径和判断标准。
- 设定边界:哪些事情可以做,哪些事情不能做,信息不足时是否允许猜测,输出是否必须保持固定格式,这些都更适合沉淀在 system 中,而不是每次临时写在 user 里。
- 统一风格与输出习惯:如果希望模型始终优先给出结构化结果、保持简洁表达、避免主观臆断,那么这些稳定规则最好由 system 长期维护。
- 减少调用漂移:当 user 的任务内容不断变化时,system 往往是整次调用中唯一相对稳定的部分。也正因为有这一层稳定约束,模型在不同任务之间的行为才不容易跑偏。
所以,从工程实现的角度看,system prompt 的意义并不是“让 Prompt 看起来更专业”,而是为模型调用建立一层基础配置和行为契约。它决定的不是某一句回答是否漂亮,而是整套系统在长期运行中能否保持相对一致、可预测、可维护的行为模式。
**如果说 user 解决的是"这一次要做什么",那么 system 解决的其实是"以后每一次都应该怎么做"。**而这两者的区别,正是聊天式使用模型与工程化使用模型之间最重要的分界线之一。
1.2.4 一次完整调用的基本结构
如果把前面的内容再收束一步,就会发现:一次真正适合工程接入的模型调用,通常不是一段孤立的 Prompt,而是由多层信息共同组成。它们各司其职:有的提供稳定规则,有的描述当前任务,有的补充背景信息,有的约束输出形式,还有的在结果返回后负责处理与兜底。

从工程角度看,这种分层不是为了把调用过程讲得更复杂,而是为了让模型行为更清晰、结果更稳定、系统更易维护。
最上面的一层通常是 system 层。这一层负责定义全局角色、规则、行为边界和输出风格。它不直接回答“当前任务是什么”,而是在更高层面规定:模型应以什么身份参与本次调用,应遵守哪些原则,在什么情况下不能随意发挥,输出应尽量呈现为怎样的风格。像“你是一名需求分析助手”“必须输出 JSON”“信息不足时不要猜测”“输出必须包含指定字段”这类要求,都更适合沉淀在 system 层。也正因为承担全局控制作用,这一层通常相对稳定,不会随每次任务轻易改变。
system 之下是 task 层。这一层对应本次调用真正要完成的任务,负责回答“现在到底要做什么”。比如分析一段需求并拆出核心模块,阅读一段代码并指出潜在风险,或根据函数说明生成一段 Python 实现。由于它直接跟随当前业务目标变化,因此是整次调用里变化最频繁的部分。换句话说,system 决定“应该以什么方式工作”,task 决定“这一次具体要完成什么”。
再往下一层是 context 层。如果说 task 告诉模型“要做什么”,那么 context 解决的是“完成这件事还需要知道什么”。在真实场景里,模型很多时候不是不会做,而是缺少必要信息。业务规则说明、代码片段、接口文档、项目结构、历史对话内容、额外约束条件,都属于 context。它的作用是补齐模型当前未知、但完成任务又必须依赖的部分。很多时候,结果质量的关键不在于 Prompt 写得多漂亮,而在于 context 给得是否准确、是否关键。
在 system、task 和 context 之后,还需要一层关键约束:format 层。这一层用于明确最终应如何输出结果。比如要求按 JSON 返回、不附加额外解释、字段必须包含 summary/risks/priority、无法判断时统一返回 unknown。format 的重要性在于,它决定输出能否被后续系统稳定消费。如果没有 format 约束,模型往往会以最自然的方式生成自由文本;而一旦结果需要进入下一模块、触发工具调用或被程序解析,结构化与可预期就会比“表达自然”更重要。
很多人在讲模型调用时,往往只讲到输入和输出为止;但从完整调用链路来看,还有最后一层经常被忽略,那就是 post-process 层。这一层虽然发生在模型返回之后,却同样属于一次完整调用不可缺少的组成部分。因为模型给出的结果并不会天然可靠地进入系统,它通常还需要经过解析、校验、兜底、过滤、重试、日志记录等一系列后处理步骤。也就是说,一次模型调用并不是“调用接口,然后拿到文本”就结束了,而是“输入设计—模型生成—输出处理”的完整闭环。没有 post-process,前面所有看似精心设计的调用,都很容易在真正接入系统时失去稳定性。
把这几层放在一起再看,你会发现,一次完整的模型调用其实已经具备了非常清晰的工程结构:system 负责稳定规则,task 负责当前目标,context 负责补足信息,format 负责约束结果,post-process 负责把结果真正接入系统。它们彼此分工不同,但缺一不可。少了 system,模型行为容易漂移;少了 task,目标会模糊;少了 context,结果会失真;少了 format,输出就难以消费;少了 post-process,系统就缺乏真正的闭环。
也正因为如此,工程化的大模型调用,从来都不是“把一句 Prompt 扔进去看看会发生什么”,而是在构造一套分层清晰、边界明确、能够被持续维护的调用结构。后面你看到的工作流、工具调用、ReAct、RAG,甚至更复杂的智能体系统,本质上也都是在这一结构之上继续扩展出来的能力形态。
1.2.5 从一次请求到一项系统能力
讲到这里,一个更实际的问题自然就出现了:什么样的模型调用方式,才真正适合进入工程系统?
答案通常不是“最聪明的调用方式”,也不是“看起来最复杂的调用方式”,而是最稳定、最可维护、最可扩展的调用方式。因为在工程场景中,模型调用的价值从来不只体现在某一次回答是否足够惊艳,更在于它能否长期稳定运行,能否被系统复用,能否在需求变化、流程扩展、上下文增加之后依然可控。
从这个角度看,工程化的大模型调用首先强调分层清晰:角色、任务、上下文、输出约束不应被揉成一段混杂的 Prompt,而应各自承担明确职责。分层越清楚,后续维护、调试与复用就越容易。反过来,如果所有内容堆在一起,一旦结果出现偏差,开发者就很难判断问题究竟出在角色定义、任务描述、上下文噪声,还是输出约束本身。
其次,工程系统通常更偏好结构化输出,而不是完全依赖自然语言。自然语言当然更灵活,也更符合人类表达习惯,但系统真正需要的往往是可解析、可校验、可传递给后续模块继续消费的结果。尤其当模型输出要进入工具调用、任务编排、状态更新或其他自动化流程时,结构化输出几乎不再是“加分项”,而是稳定落地的前提。换句话说,模型输出的价值不仅在于“说得对不对”,也在于“能不能被系统接住”。
再进一步,上下文管理必须是有选择的,而不是越多越好。很多人在初学阶段倾向于把一切可能有用的信息都塞给模型,仿佛上下文越丰富,结果就越可靠。但真实情况往往相反:上下文越长,噪声也越多;信息越杂,模型越难抓住重点。更有效的做法不是“尽可能多给”,而是“给足够关键的信息”。也就是说,好的上下文设计不是简单堆材料,而是一种筛选能力:你需要清楚哪些信息真正决定任务质量,哪些内容只是增加干扰。
与此同时,工程化调用也意味着不能把模型输出当作天然可靠的结果去盲目信任。模型不是数据库,也不是严格确定性的函数调用。它的输出天然带有生成式的不确定性,因此任何真正进入系统的结果,都应该经过解析、校验、兜底与过滤,必要时还要配合重试与日志记录。只有这样,模型调用才不会因为一次偶然的异常输出就把问题扩散到整个流程里。也正因为如此,工程化使用模型的关键不是“如何让模型永远不犯错”,而是“如何让系统能够识别、约束并消化这些不确定性”。
最后,真正适合工程化开发的调用方式通常不会散落在各处业务代码里。因为如果每个功能模块都各自拼接 Prompt、各自解析结果、各自处理异常,随着调用次数增加,系统很快就会混乱且难以维护。更合理的做法是把模型调用逐步封装成统一的能力层:将角色定义、输出格式、日志记录、异常处理、结果校验等能力逐步标准化,让模型调用从“开发者临时在调模型”,变成“系统内部可复用的一项能力”。
走到这里,我们其实已经可以回到本节最核心的判断:调用模型,本质上是在设计一次受约束的对话。
这里所谓“对话”,意味着它并不是一个机械、严格、固定的函数输入输出关系,而是带有语言理解、上下文关联和生成过程的动态交互;而所谓“受约束”,则意味着它不能完全依赖模型自由发挥,而必须通过角色、任务、上下文和输出格式,把这次交互尽可能收束在一个可控范围内。
也正因为如此,一次模型调用看上去简单,实际上已经包含了智能体开发最底层的几个关键问题:如何定义角色,如何描述任务,如何补足上下文,如何限制输出,以及如何让结果进入系统。后面你看到的 Prompt Engineering、结构化输出、工作流编排、工具调用,甚至 ReAct 和 RAG,本质上都是围绕这些基础问题,继续向前扩展出来的更高层能力。
所以,对开发者来说,真正重要的并不是“会不会调模型”,而是能不能把一次调用设计得足够清楚、足够稳定、足够可复用。因为只有做到这一点,模型才不只是一个会说话的黑盒,而会逐渐成为一个能够接入系统、支撑流程、参与任务的软件能力单元。
而要把这套“调用结构”真正变成可控的工程接口,下一步就必须回答:我们到底靠什么去控制 system/task/context/format 这些层?这就是 Prompt 的工作。
1.3 Prompt:不是提问,而是控制
上一节我们把“模型调用”拆成了几层:system 负责稳定规则,task 负责当前目标,context 补足关键信息,format 约束输出形式,最后再由 post-process 把结果接回系统。
接下来一个自然的问题是:这些层级到底靠什么来“控制”模型?答案就是 Prompt——更准确地说,是把 Prompt 当作一份行为契约:它把角色、任务、上下文与输出接口都写成可执行的约束。
很多人在第一次接触大模型时,会把 Prompt 当作一种“更高级的提问方式”。提问越清楚,回答越准确;描述越具体,输出越接近预期。这种理解并不算错。但如果 Prompt 只停留在“如何更会问问题”的层面,就会低估它在智能体开发中的真实作用。
在工程语境下,Prompt 的本质不是聊天技巧,而是一种轻量级的软件接口设计。
在传统软件开发中,我们会用函数签名、参数约束、返回结构和异常处理来规定一项能力如何被调用;而在大模型系统里,Prompt 承担了类似的职责。它不只是“告诉模型你想要什么”,更是在更深层面规定:模型是谁,要做什么,需要知道什么,应当如何返回结果,以及在什么情况下必须停止猜测并承认不确定。
也因此,一个好的 Prompt 并不是“看起来像在和 AI 好好说话”,而更像一份边界清晰、意图明确、行为可控的接口定义。
1.3.1 角色设定:先定义模型“是谁”
在 Prompt 设计中,首先要解决的往往不是“让模型怎么答”,而是“让模型以什么身份来答”。
模型本质上是通用生成器。如果不对角色做限定,它会根据当前输入自行推断回答方式:有时像老师,有时像顾问,有时像程序员,有时又像泛泛而谈的聊天助手。轻量场景下,这种自由度也许问题不大;但一旦进入工程场景,角色漂移就会直接造成输出风格和判断标准不一致。
比如下面这个需求分析任务:
帮我分析这个需求:用户上传 Excel 后,系统自动解析数据并生成日报。
如果不设定角色,模型很可能给出一段泛泛的文字描述,比如:
这个需求主要是实现文件上传、数据解析和日报生成,建议考虑文件格式校验和错误处理。
这段回答不能说错,但过于笼统,既难以直接进入系统,也很难支撑后续流程。
如果加上角色设定:
你是一名资深产品技术分析助手,请从系统设计角度拆解需求,输出核心模块、潜在风险和待确认问题。
输出通常会明显收束,更适合工程使用,例如:
- 核心模块:文件上传、Excel 解析、数据校验、日报生成、失败通知
- 潜在风险:文件格式不统一、字段缺失、数据量过大导致解析超时
- 待确认问题:日报模板是否固定、是否支持多种 Excel 格式、异常数据如何处理
角色设定的价值不在于让模型“演得像”,而在于让分析视角更稳定,让输出更贴近使用场景。
1.3.2 任务定义:再告诉模型“要做什么”
角色解决的是“它是谁”,任务定义解决的是“这次到底要完成什么”。
很多时候,模型输出不理想并不是因为能力不够,而是任务本身描述得不够清楚。比如:
帮我看下这个功能怎么做。
这种说法在人与人沟通时,也许还能靠背景经验补足;但对模型来说,它并不知道你要的是产品拆解、技术实现、数据库设计,还是测试方案。
如果把任务定义得更具体:
请将下面这个需求拆解为 3 个后端模块,并说明每个模块的职责:
“用户上传 Excel 后,系统自动解析数据并生成日报。”
目标就清楚得多,模型也更容易输出你真正需要的内容,而不是在多个方向上自由发挥。
再比如代码生成场景,下面这个 Prompt:
帮我写一个函数。
几乎没有工程价值。因为函数做什么、输入是什么、输出是什么、边界条件是什么,全都没有交代。
如果改成:
请使用 Python 编写一个函数,输入为用户列表,返回年龄大于 18 岁用户的邮箱地址。
要求:
1. 忽略没有邮箱的用户
2. 忽略年龄为空的数据
3. 返回结果为字符串列表
这时才算真正完成了任务定义。
所以,Prompt 设计里最常见的问题之一不是语言不够优美,而是任务定义不够明确。你说不清楚自己要什么,模型自然也难以给出稳定结果。
1.3.3 上下文补充:让模型知道它原本不知道的部分
角色和任务明确之后仍然不够。模型再强,也不可能天然知道你项目里的真实背景。
比如让模型生成一个接口实现:
请为日报生成模块设计一个 API。
任务本身没问题,但如果没有任何上下文,模型只能基于通用经验给出“常见接口设计”。而在真实项目中,往往还依赖很多额外信息:
- 系统是 REST 还是 GraphQL
- 使用什么鉴权方式
- 日报模板是否固定
- 是否需要异步任务
- 现有项目的命名规范是什么
如果不提供这些信息,模型就只能“猜一个差不多的版本”。
比如补上上下文:
背景信息:
1. 当前项目采用 RESTful 风格
2. 所有接口都需要 JWT 鉴权
3. 日报生成是异步任务
4. 任务提交后返回 task_id
5. 接口字段使用 snake_case 命名
这时,输出通常会更贴近你的系统,而不是泛化模板。
在 AI Coding 场景中,这一点尤其重要。
因为代码开发几乎总是高度依赖上下文:项目结构、已有模块、接口约定、团队规范、业务规则都会直接影响生成结果。很多时候,模型“写得不够好”不是因为不会,而是因为你没有把真正需要的信息交给它。
因此,上下文的价值不在于多,而在于关键。好的上下文不是“把所有资料都塞进去”,而是筛出那些真正影响判断的要点。
1.3.4 输出约束:让结果从“能看”变成“能接”
角色、任务和上下文决定模型如何理解这次调用;输出约束则决定结果最终应当长什么样。
这一层在工程场景里非常关键。因为模型输出往往不只是给人看,还要进入系统、触发工具调用、传递到下一个流程节点。仅仅“写得通顺”远远不够,还需要“返回得可消费”。
比如下面这个 Prompt:
请分析这个需求,并给出结果。
模型可能会返回一大段自然语言,阅读也许没问题,但程序很难继续处理。
如果改成:
请分析下面这个需求,并按 JSON 返回:
{
"summary": "",
"modules": [],
"risks": [],
"questions": []
}
需求:用户上传 Excel 后,系统自动解析数据并生成日报。
模型就更可能返回可解析、可传递、可继续消费的结构化结果。
再比如代码生成场景。如果你只说:
帮我写一个 Python 函数。
模型可能会输出:
- 一段解释
- 一段代码
- 以及一段注意事项
这些对人类阅读不算问题,但对程序接入并不友好。
如果进一步约束:
请只输出 Python 代码,不要附加任何解释,也不要使用 Markdown 代码块。
结果就更接近“可被系统直接接住”的形式。
从工程视角看,输出约束就是在为模型返回值定义接口结构。没有输出约束的 Prompt 更像一次聊天;有了输出约束,才开始接近一次真正的系统调用。
1.3.5 边界限制:控制模型不要做什么
很多人在设计 Prompt 时,只关注“要模型做什么”,却很少定义“不要模型做什么”。但在智能体开发里,这恰恰是关键的一层。
因为大模型的默认倾向是尽量给出完整、流畅、看起来合理的回答。它不喜欢留白,也不天然喜欢说“不知道”。如果没有边界限制,它会在信息不足时继续猜测,在任务模糊时继续延展,在格式不清时自由发挥。
比如你问:
请根据以下日志判断问题原因,并给出修复方案。
如果日志信息并不充分,模型仍可能给出一个“听起来挺像那么回事”的原因分析。
这时最危险的不是完全胡说,而是半对半错,足以误导后续处理。
因此,更合理的 Prompt 往往需要加入边界限制:
如果日志信息不足以确定原因,请明确返回 “信息不足,无法判断”,不要猜测具体故障点。
同样,在需求分析、代码生成、接口设计等场景中,也经常需要明确告诉模型:
- 不要编造不存在的字段
- 不要补充未给出的业务规则
- 不要输出额外解释
- 信息不足时要明确说明不确定
这一层很像传统软件中的参数校验与异常约束。
它的作用不是让模型“少做事”,而是让模型在工程系统里学会安全失败,而不是“不知道也硬编”。
1.3.6 一个简单示例:把“聊天式 Prompt”变成“工程式 Prompt”
下面是一种常见但偏聊天式的写法:
帮我分析一下这个上传 Excel 生成日报的需求。
帮我分析一下这个上传 Excel 生成日报的需求。
这个 Prompt 的问题不在于模型答不出来,而在于每一层都太松散:没有角色设定、任务目标不具体、没有上下文、没有输出约束,也没有边界限制。因此,输出结果会高度依赖模型临场发挥。
如果把它改造成更工程化的版本:
你是一名产品技术分析助手。
请分析以下需求,并输出结构化结果:
需求:用户上传 Excel 后,系统自动解析数据并生成日报。
要求:
1. 从系统设计角度拆解需求
2. 输出字段必须包含:
- summary
- modules
- risks
- questions
3. 每个 modules 元素需包含 name 和 responsibility
4. 如果信息不足,请在 questions 中提出,而不要自行补充业务规则
5. 最终按 JSON 返回,不要输出额外解释
你会发现,这个 Prompt 已不再只是“问问题”,更像是在定义一次接口调用。
1.3.7 Prompt 设计,不是在“哄模型”,而是在定义行为契约
讲到这里,可以回到本节的核心判断:Prompt 并不是聊天技巧,而是一种轻量级的软件接口设计。
它定义的不是一句话该怎么说得更漂亮,而是一次调用中的行为契约:以什么身份工作,要完成什么任务,可以依赖哪些信息,必须按什么方式返回,以及在什么边界条件下必须停下。
也正因为如此,写 Prompt 不是“把一句话说得更漂亮”,而是把模型行为从模糊、自由、开放的生成,逐步收束为一个边界清晰、职责明确、输出可控的能力单元。
1.4 漂移:模型为什么会不稳定
如果把 Prompt 看作“控制杆”,那么这一节讨论的就是:控制不到位时,系统会以哪些方式失控。
几乎所有人在真正开始用大模型做开发任务时,都会遇到类似的问题:同样的问题,结果却不一样;前几次还能按要求输出 JSON,下一次却突然夹带解释;有时回答很谨慎,有时又过度发挥;有些内容看起来很合理,但仔细检查后却发现是错的。
这些现象很容易让人觉得模型“不稳定”是一种玄学。但从开发视角看,所谓“不稳定”往往不是单一原因造成的,而是多个因素叠加后的结果。也就是说,模型的不稳定并非不可理解,而是可以被拆解、被分析,并逐步收敛的问题。
1.4.1 不稳定通常表现在哪些地方

最常见的几类问题大致如下:
- 结果不一致:同样的问题,一次回答偏技术拆解,另一次偏产品描述,粒度和重点都可能变化。
- 格式漂移:你要求输出 JSON,模型前几次都能做到,但某一次突然加上一段自然语言说明,或者漏了字段。
- 内容跑题:你要它分析风险,它却开始讲背景;你要它只返回代码,它却附带一堆解释。
- 看似合理实则有误:最危险的一类——不是明显胡说,而是用流畅、自信的方式补出并不存在的规则、字段或结论。
- 表现忽左忽右:有时过于保守,动不动就说"信息不足";有时又补全过度,把没给的信息说得像真的一样。
这些问题在聊天场景里可能只是体验不够好,但在智能体系统里会直接影响流程执行,因为模型输出往往不是终点,而是后续模块的输入。
1.4.2 温度参数会放大还是收敛波动
影响稳定性的一个直接因素是 temperature(温度)。
可以简单理解为:温度越高,模型越倾向于给出更多变化和探索;温度越低,模型越倾向于选择更稳妥、更一致的输出。
如果是创意写作、文案发散这类任务,稍高的温度有时是有帮助的;但如果是结构化分析、代码生成、参数提取这类工程任务,高温度往往会放大波动。
所以在工程场景里,一个基本原则是:
- 越强调稳定和可解析,温度越应该保守
- 越强调创意和探索,温度才可以适当放开
当然,调低温度只能减少随机性,不能替代清晰的 Prompt。如果任务本身就很模糊,低温度最多只是让模型“更稳定地模糊”。
1.4.3 上下文不足时,模型只能“靠猜”
另一个常见原因是上下文不够。
比如你直接写:
请为日报模块设计一个接口。
这个请求表面上很明确,但实际上缺了很多关键信息:系统风格是什么、是否需要鉴权、是同步还是异步、字段命名是否有规范、返回结构如何设计。
如果这些都没说,模型只能基于通用经验去补全;而在不同轮次里,它补全的方式可能并不一致,于是你就会觉得结果“不稳定”。
很多时候,模型并不是乱答,而是在填补你没有给出的空白。空白越多,波动就越大。
1.4.4 Prompt 不清楚,任务就天然是开放的
除了上下文不足,另一个更常见的问题是任务定义不清楚。
例如:
帮我分析这个功能。
这里的“分析”到底是指需求拆分、风险判断、技术方案还是测试策略?模型并不知道。
只要任务存在多种合理解释,输出就很难稳定。
再比如:
帮我写一个接口。
这同样是一个开放任务:语言没说、框架没说、输入输出没说、异常处理没说,模型自然只能按自己的理解发挥。
所以很多输出漂移,并不是模型“不听话”,而是任务本身就是一道开放题。
你越明确自己要什么,模型越容易稳定地给出那个结果。
1.4.5 输出约束缺失,会直接导致格式漂移
如果说上下文不足主要影响内容,那么输出约束缺失主要影响格式。
比如你希望模型返回这样的结构:
{
"summary":"",
"risks": [],
"questions": []
}
如果你没有明确要求“必须按该结构返回,且不要附加额外解释”,模型就很可能在前面先写一段自然语言,再附上一个类似 JSON 的结构,甚至顺手改掉字段名。
这类问题在人工阅读时也许还能容忍,但一旦进入程序处理,就会直接导致解析失败。
所以,输出稳定性的本质之一,就是约束强度。
你对格式、字段、默认值和异常情况定义得越清楚,模型的输出空间就越容易被收束。
1.4.6 一个简单对比例子
看一个最直观的例子。
下面这个 Prompt:
帮我分析这个需求并给出结果。
模型当然能回答,但很容易出现这些问题:结果粒度不一致、风险和问题混在一起、输出结构不固定、会补出并不存在的业务规则。
如果改成:
你是一名需求分析助手。
请分析以下需求:
“用户上传 Excel 后,系统自动解析数据并生成日报。”
要求:
1. 从系统设计角度输出结果
2. 必须包含 summary、modules、risks、questions 四个字段
3. modules 中每一项必须包含 name 和 responsibility
4. 如果信息不足,不要补充业务规则,而是在 questions 中提出
5. 最终按 JSON 返回,不要输出额外解释
你会发现,即使模型仍然不是百分之百完美,它的输出范围也会明显收束。
这说明所谓“稳定性”,很多时候并不是模型突然变好了,而是调用环境被设计得更清楚了。
它通常来自几个常见来源:温度过高带来的随机性、上下文不足导致的猜测、任务定义不清造成的多义性、输出约束不够导致的格式漂移,以及缺乏校验机制带来的错误扩散。
一旦把这些问题拆开看,很多“不稳定”其实都能逐步优化:
- 用更合适的温度降低随机波动
- 用关键上下文减少模型猜测
- 用清晰任务定义减少目标偏移
- 用输出约束降低格式漂移
- 用校验和兜底机制控制错误传播
真正的工程能力,不是要求模型永远不犯错,而是把这种不确定性收束到系统可以接受的范围内。
1.5 结构:让输出真正进入系统
前面我们已经有了“调用结构”(1.2)和“控制方式”(1.3),也看到了失控的典型表现(1.4)。但还有一个更硬的工程门槛:就算内容大体正确,只要输出形态不可解析、不稳定,系统依然接不住。
因此这一节只回答一个问题:为了进入系统,模型输出为什么必须进一步结构化?
在聊天场景里,我们通常不会太在意这一点。只要模型回答通顺、逻辑清楚、内容看起来靠谱,人就能继续理解和判断。也就是说,在“人读人用”的场景里,自然语言往往已经足够。
但一旦进入智能体开发和系统接入,情况就完全不同了。
这时,模型输出往往不再只是给人看的文本,而是要继续进入:
- 下一个任务节点
- 工具调用参数
- 工作流状态
- 数据存储结构
- 代码执行流程
- 评测与日志系统
也就是说,模型结果必须从“能读懂”进一步变成“能处理”。而实现这一点的关键,就是结构化输出。

1.5.1 为什么自然语言输出不适合直接接系统
自然语言最大的优点是灵活,最大的缺点也是灵活。
对人来说,这种灵活性很友好。你可以容忍一句话多几个修饰词,也可以容忍它换一种说法,甚至顺序乱一点、表达绕一点,依然能明白它的意思。但对系统来说,这种灵活性往往就是问题的来源。
例如,你希望模型帮你分析一个需求,如果只是让它自由输出,它可能会返回这样一段结果:
这个需求大致可以分成三个部分,首先是文件上传,其次是数据解析,最后是日报生成。风险主要在于文件格式兼容性和异常数据处理上,另外还有一些待确认问题,比如日报模板是不是固定。
这段话对人类来说完全没问题,但一旦交给程序继续处理,就会立刻遇到困难:
- 哪一段是模块,哪一段是风险,哪一段是待确认问题?
- 模块到底是 3 个还是更多?
- 风险是一个字符串,还是一个数组?
- 待确认问题是一句话,还是一个问题列表?
- 如果下一步要把“风险”交给另一个 Agent,它该从哪里开始切分?
问题不在于自然语言“不够聪明”,而在于它边界不明确、结构不固定、机器难以稳定解析。
所以,自然语言很适合直接面向人,但并不适合直接面向系统。系统需要的不是“差不多能懂”,而是“足够明确,能被稳定读取”。
1.5.2 结构化输出的本质:不是更整齐,而是更可消费
很多人第一次接触结构化输出时,会以为它只是让结果“更好看”。
但从工程角度看,结构化输出的意义并不在于整齐,而在于可消费。
所谓可消费,是指模型的结果不再只是一个文本段落,而是一个有边界、有字段、有层级关系的结果对象。它可以被程序读取,被下一个模块继续使用,被工具调用直接接住,甚至被记录进状态系统和日志系统。
比如,同样一个需求分析任务,如果我们让模型按 JSON 返回:
{
"summary":"用户上传 Excel 后,系统自动解析数据并生成日报",
"modules": [
{"name":"upload", "responsibility":"处理 Excel 上传与基础校验"},
{"name":"parser", "responsibility":"解析 Excel 内容并提取有效数据"},
{"name":"report", "responsibility":"根据解析结果生成日报"}
],
"risks": [
"文件格式不统一",
"字段缺失导致解析失败",
"大文件可能导致处理超时"
],
"questions": [
"日报模板是否固定",
"是否支持多种 Excel 格式"
]
}
这时,输出就不再只是一段“给人看的回答”,而成为一个可以被系统继续处理的结构:
- summary 可以直接展示给前端
- modules 可以进入后续架构拆分流程
- risks 可以交给风险评估模块
- questions 可以进入需求澄清清单
也就是说,结构化输出第一次让模型结果从“内容”变成了“对象”。
1.5.3 为什么 JSON 会成为最常见的结构化形式之一
谈结构化输出,几乎绕不开 JSON。
原因并不是 JSON 本身多高级,而是它处在一个非常合适的位置:对人类足够易读,对程序足够易解析。
在工程实践里,JSON 有几个天然优势。
第一,层次关系清楚。对象、数组、字段和值,天然适合描述任务结果。
第二,兼容性好。无论是前端、后端、脚本系统,还是工作流平台、日志系统、数据库中间层,几乎都能自然处理 JSON。
第三,通用性强。同一份结构化结果,既能给人阅读,也能给系统接入,不需要为“人类可读”和“机器可读”分别维护两套格式。
因此,在很多智能体系统里,JSON 不是唯一选择,但通常是最稳妥的默认选项。尤其当模型输出要进入工具调用、状态更新或多步骤编排时,JSON 往往是天然起点。
但要注意:要求模型输出 JSON,并不等于它就一定会稳定输出正确的 JSON。
这也是为什么结构化输出不能只停在“按 JSON 返回”,还需要继续讨论字段约束、异常处理和结果校验。
1.5.4 结构化输出并不只有 JSON
虽然 JSON 最常见,但结构化输出并不等于只能输出 JSON。更准确地说,JSON 只是“最通用的默认方案”,而不是唯一答案。真正重要的不是格式本身,而是输出形式是否与下游消费场景匹配。
在某些需要与旧系统、配置协议或历史接口兼容的场景里,XML 仍然很有价值。它的标签层次更显式,更适合本身就依赖 XML 的对接场景。
如果输出目标更偏向“配置文件”或“人类后续编辑”,那么 YAML 往往更自然。它比 JSON 更简洁,也更适合部署脚本、流水线定义、配置生成等场景。
对于天然是二维表的数据整理任务,例如批量字段导出、测试样例列表、简单报表汇总,CSV 反而可能更直接。它表达能力不如 JSON 丰富,但在规则明确、列结构固定的任务里很实用。
还有一类经常被忽略、但同样重要的结构化结果,是 choice / route 类输出。这类输出不需要模型生成复杂内容,而是让它在有限选项中做出明确判断,例如:
- 当前任务属于 code_review、bug_fix 还是 requirement_analysis
- 下一步进入 tool_call、ask_user 还是 finish
- 当前任务应该 handoff 给哪个子 Agent
这种输出本质上也是结构化的,只是更轻、更偏决策,而不是偏内容。在很多工作流系统里,这类输出比完整 JSON 更高效,因为系统真正需要的不是长篇解释,而是一个明确的路由信号。
除此之外,还有一些半结构化形式也很常见,比如:
- Markdown 表格
- 固定字段文本
- 只输出 SQL / Python / Shell 代码
- 只输出某种约定格式的 DSL
它们未必像 JSON 那样通用,但只要边界清楚、格式稳定,同样可以成为工程链路中的有效输出。
因此,结构化输出真正重要的不是“统一用一种格式”,而是:让模型结果以最适合下游系统接收的形式返回。
1.5.5 输出格式正确,本身就是能力的一部分
很多初学者会把模型能力理解为“内容对不对”。
但在工程场景里,这个判断并不完整。因为一个输出结果即使内容大体正确,只要格式不对、字段缺失、层级混乱、无法解析,它在系统里依然不可用。
也就是说,输出格式正确,本身就是能力的一部分。
例如你要求模型返回:
{
"action":"",
"params": {}
}
如果模型返回了:
我建议执行
generate_report这个动作,参数可以是用户上传的文件路径。
这段话从语义上不算错,但在系统里毫无用处。因为程序真正需要的是:
{
"action":"generate_report",
"params": {
"file_path":"/tmp/demo.xlsx"
}
}
差别不在于模型懂不懂任务,而在于它有没有把结果组织成系统真正需要的形式。
所以,当我们谈“模型输出能力”时,至少要包含两层含义:
- 内容层面:它说得对不对
- 结构层面:它返回得能不能用
少了后者,前者再好,也很难真正落地。
1.5.6 字段约束与异常处理:让结构真正稳定下来
只告诉模型“请按 JSON 返回”,往往还不够。
因为 JSON 只是最外层的壳,真正决定系统能否稳定接入的,是字段本身是否被约束清楚。
比如下面这个 Prompt:
请分析需求,并按 JSON 返回。
即使模型真的输出了 JSON,不同轮次里也可能出现这些变化:
- 一次字段叫 modules,另一次变成 components
- 一次 risks 是数组,另一次变成一个长字符串
- 一次有 questions,另一次漏掉了
- 一次模块对象里有 name 和 responsibility,另一次只剩下 desc
也就是说,没有字段约束的结构化输出,仍可能只是“看起来像结构化”,但并不稳定。
更工程化的写法通常会进一步明确:
- 必须包含哪些字段
- 字段的类型是什么
- 字段缺失时应该返回什么
- 是否允许空数组
- 是否允许空字符串
- 子字段是否固定
例如:
请按 JSON 返回,且必须满足以下要求:
1. 顶层字段必须包含 summary、modules、risks、questions
2. modules 必须是数组
3. modules 中每个元素必须包含 name 和 responsibility 两个字段
4. risks 与 questions 必须为字符串数组
5. 如果没有内容,请返回空数组,不要省略字段
到这一步,模型的输出空间才真正开始被收束。
也只有到这一步,结构化输出才从“有格式”走向“真正可接系统”。
1.5.7 异常处理:不是让模型永远正确,而是让系统能处理错误
结构化输出还有一个常被忽略的部分:异常处理。
很多人一提结构化输出,就默认模型应该永远按格式返回、永远不出错。但真实情况是,只要是生成式模型,就一定会出现失败、缺字段、格式漂移、值不合法等情况。问题不在于如何彻底消灭这些问题,而在于系统有没有为这些问题预留处理方式。
因此,工程化的大模型调用不能停在“请按 JSON 返回”,还要继续考虑:
- 如果模型返回的 JSON 解析失败怎么办?
- 如果字段缺失怎么办?
- 如果字段类型不对怎么办?
- 如果内容明显超出约束怎么办?
- 如果模型说“无法判断”,系统怎么接这个结果?
举个例子,如果你要求模型在信息不足时返回:
{
"summary":"",
"modules": [],
"risks": [],
"questions": ["信息不足,无法完成完整分析"]
}
那么即使它没法完成完整任务,系统也依然能接住这个结果。
这比让模型在失败时自由发挥、输出一段杂乱的自然语言,要安全得多。
所以,异常处理的本质不是要求模型“别犯错”,而是要求系统提前定义:如果它出错,应该怎样安全地失败。
1.5.8 一个简单对比例子
下面这种写法更接近聊天式使用模型:
请帮我分析这个需求,并告诉我可能有哪些模块和风险。
模型通常会返回一段自然语言。人读起来问题不大,但系统很难稳定处理。
如果改成:
你是一名需求分析助手。
请分析以下需求:
“用户上传 Excel 后,系统自动解析数据并生成日报。”
请按 JSON 返回,字段要求如下:
- summary: string
- modules: array,每项包含 name 和 responsibility
- risks: string array
- questions: string array
要求:
1. 不要输出额外解释
2. 如果信息不足,不要补充业务规则,而是在 questions 中提出
3. 所有字段必须返回,不允许省略
这时得到的就不再只是一个“回答”,而是一个可被系统继续传递的结果对象。
两者最大的区别不是模型是否更聪明,而是第二种写法已经把模型从“聊天对象”推进到了“接口能力”。
讲到这里,这一节最核心的结论就很清楚了:
结构化输出的意义,不是让模型看起来更规整,而是让模型第一次真正具备了进入系统的能力。
自然语言适合人类交流,但系统需要的是边界清晰、字段稳定、可解析、可校验的结果。JSON 是最常见的形式,但不是唯一形式;字段约束与异常处理,才是让结构化输出真正稳定可用的关键。
也正因为如此,结构化输出不是 Prompt 设计里的附加技巧,而是智能体开发迈向工程化的第一道门槛。只有当模型输出能够被系统稳定接住,后面的 Tool Calling、工作流编排、ReAct、状态管理,才真正有继续往下搭建的基础。
换句话说,聊天式使用模型关注的是“它会不会说”;而系统式接入模型关注的是“它返回的东西能不能被用”。这两者看似只差一步,实际上正是从“体验 AI”走向“开发智能体”的分界线。
1.6 封装:从一次调用到一个可复用模块
到这里,我们已经把一次调用的“输入—约束—输出”收束到了可解析、可校验的结构化结果上(见 1.5)。这一节要做的,是把这种“能跑通的一次调用”,进一步整理成项目里可维护、可复用、可扩展的一项能力模块。但如果内容停在这里,读者学到的仍然更像一套“如何把一次调用写对”的方法,而不是“如何把这项能力真正放进项目里”。
一旦进入工程实践,关注点就会立刻变化。此时真正重要的问题,已经不再是“这次调用能不能成功”,而是:
- 这段 Prompt 以后好不好改
- 结果异常时系统怎么处理
- 不同业务是否在重复编写相同的调用逻辑
- 后续如果要加入链式调用、工具调用或会话状态,现在这段代码是否还能接得住
也正因为如此,模型调用真正进入工程系统的标志,并不是"会调 SDK",而是能把一次调用收束成一个可维护、可复用、可扩展的模块。
这一节不再继续讲概念,而是直接落到一个最基本、也最常见的实战问题上:如何用 TypeScript 和最基础的 OpenAI SDK,把一个需求分析能力从“一段能跑的示例代码”整理成“项目里可复用的一项能力”。

1.6.1 从一个最小可运行版本开始
任何工程化封装的第一步,都不应该是过早抽象。先把最小链路跑通,再决定哪些部分值得抽出来。用 TypeScript 写,一个最简单的版本大概是这样:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: "https://api.amux.ai/v1"
});
async function main() {
const response = await client.responses.create({
model: "gpt-5.2",
input: "请分析这个需求:用户上传 Excel 后,系统自动解析数据并生成日报。",
});
console.log(response.output_text);
}
main().catch(console.error);
这段代码本身没有问题,它已经完成了最底层的三件事:
- 创建客户端
- 调用模型
- 获取结果
但如果项目长期停留在这种状态,很快就会遇到一个典型问题:业务逻辑、Prompt、模型参数、输出处理全都混在一起。它适合验证想法,却不适合长期维护。所以下一步不是继续往这个函数里塞逻辑,而是先把职责拆开。
1.6.2 不要把 Prompt 写死在业务代码里
很多 AI 项目的第一版,都会长成下面这样:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: "https://api.amux.ai/v1"
});
export async function analyzeRequirement(requirement: string): Promise<string> {
const prompt = `
你是一名需求分析助手。
请分析以下需求,并给出模块与风险:
${requirement}
`.trim();
const response = await client.responses.create({
model: "gpt-5.2",
input: prompt,
});
return response.output_text;
}
这段代码的问题不是不能用,而是所有东西都被绑死了:
- Prompt 写死在函数里
- 角色设定和任务描述揉在一起
- 业务函数直接依赖模型返回的自由文本
- 后面想加日志、重试、输出校验时,只能继续往里堆
当系统里只有一个调用函数时,这种写法还勉强可控。但一旦有需求分析、日志诊断、代码生成、SQL 提取、任务路由等多个能力,这种写法很快就会演变成一堆“各写各的 Prompt 函数”。
更合理的方式,是先把模型调用中最容易复用的部分独立出来。哪怕只是一个很小的目录划分,也能让代码立刻更有工程感:
src/
llm/
client.ts
prompts.ts
schemas.ts
validators.ts
services/
requirementAnalyzer.ts
1.6.3 先定义结果,而不是直接消费自由文本
真正让模型调用开始工程化的,不是先抽客户端,而是先明确:这次返回的结果应该长什么样。
如果你还在直接消费一段自然语言,那么模型更像一个聊天对象。而当你开始定义结果结构时,它才逐渐变成一个接口能力。TypeScript 本身就很适合做这件事,因为你可以先把返回对象的类型定义清楚。
比如先定义一个需求分析结果:
// src/llm/schemas.ts
export interface ModuleItem {
name: string;
responsibility: string;
}
export interface RequirementAnalysis {
summary: string;
modules: ModuleItem[];
risks: string[];
questions: string[];
}
这一步看起来普通,但它其实完成了一个关键转变:你不再只是“让模型给点东西”,而是在明确规定“系统最终要接什么”。从工程思维上看,这和先设计函数返回值、再实现函数本体,是同样的逻辑。
1.6.4 封装一个统一的模型调用层
有了结果结构之后,下一步再把“发请求、拿结果、做解析”这件事抽成一个统一调用层,代码就会顺很多。
// src/llm/client.ts
import OpenAI from "openai";
export class LLMClient {
private client: OpenAI;
private model: string;
constructor(model = "gpt-5.2") {
this.client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: "https://api.amux.ai/v1"
});
this.model = model;
}
async generateText(systemPrompt: string, userPrompt: string): Promise<string> {
const response = await this.client.responses.create({
model: this.model,
input: [
{
role: "system",
content: [{ type: "input_text", text: systemPrompt }],
},
{
role: "user",
content: [{ type: "input_text", text: userPrompt }],
},
],
});
return response.output_text;
}
async generateJSON<T>(systemPrompt: string, userPrompt: string): Promise<T> {
const text = await this.generateText(systemPrompt, userPrompt);
try {
return JSON.parse(text) as T;
} catch {
throw new Error(`模型输出不是合法 JSON: ${text}`);
}
}
}
这层抽象的价值在于,它先把最底层的共性能力统一起来了:
- 统一创建客户端
- 统一指定模型
- 统一组织 system / user 输入
- 统一拿到文本结果
- 统一做 JSON 解析
这还不是最终形态,但已经完成了很重要的一步:模型调用不再散落在各处业务函数里,而开始有了统一入口。
1.6.5 Prompt 也要像代码一样可维护
到这里,业务代码已经不应该继续直接拼 Prompt 了。因为一旦 Prompt 嵌在业务逻辑里,后面想改 system 规则、加字段约束、做版本管理,都会非常痛苦。
更好的方式,是把 Prompt 也抽成系统资源。
// src/llm/prompts.ts
export const REQUIREMENT_ANALYSIS_SYSTEM_PROMPT = `
你是一名需求分析助手。
你必须严格按 JSON 返回。
如果信息不足,不要猜测,而是在 questions 字段中提出。
不要输出额外解释。
`.trim();
export function buildRequirementAnalysisPrompt(requirement: string): string {
return `
请分析以下需求,并输出结构化结果。
需求:
${requirement}
返回字段要求:
1. summary: 需求摘要
2. modules: 模块列表,每个元素包含 name 和 responsibility
3. risks: 风险列表
4. questions: 待确认问题列表
`.trim();
}
以后你如果想调整 system 约束、补字段、收紧边界,或者比较不同 Prompt 的效果,都可以在这一层完成,而不会把业务逻辑搅乱。
1.6.6 把业务能力真正收束成一个模块
到这里,调用层和 Prompt 层都准备好了,业务模块就会变得很干净。它不再负责“怎么调模型”,只负责“我要什么能力”。
// src/services/requirementAnalyzer.ts
import { LLMClient } from "../llm/client";
import {
REQUIREMENT_ANALYSIS_SYSTEM_PROMPT,
buildRequirementAnalysisPrompt,
} from "../llm/prompts";
import type { RequirementAnalysis } from "../llm/schemas";
export class RequirementAnalyzer {
private llm: LLMClient;
constructor(model = "gpt-5.2") {
this.llm = new LLMClient(model);
}
async analyze(requirement: string): Promise<RequirementAnalysis> {
const userPrompt = buildRequirementAnalysisPrompt(requirement);
return this.llm.generateJSON<RequirementAnalysis>(
REQUIREMENT_ANALYSIS_SYSTEM_PROMPT,
userPrompt,
);
}
}
真正使用时,大致就是这样:
// src/index.ts
import { RequirementAnalyzer } from "./services/requirementAnalyzer";
async function main() {
const analyzer = new RequirementAnalyzer();
const result = await analyzer.analyze(
"用户上传 Excel 后,系统自动解析数据并生成日报。",
);
console.log(result);
}
main().catch(console.error);
这时候你拿到的就不再是一段“看起来不错的文本”,而是一个已经有明确结构约定的结果对象。也就是说,这项能力已经开始像一个正常的软件模块那样工作了。

1.6.7 结果校验、兜底和重试,不是附加项
很多人写到上面这一步,会觉得“已经够用了”。
但如果真的进入项目,你会很快发现,真正拉开差距的不是“能不能调通”,而是“出问题以后系统还能不能稳住”。
最基本的几件事,建议在这一层就考虑进去。
第一,JSON 解析失败要有明确报错。如果模型返回的不是合法 JSON,不能悄悄吞掉,也不要直接返回 null,而应该知道失败到底发生在哪一步。
第二,结果结构要做校验。即使成功 JSON.parse,模型仍然可能漏字段、改字段、返回错误类型。
第三,允许安全失败。如果信息不足,不一定非要硬生成一个“差不多”的结果。很多时候,更合理的是返回一个结构化失败结果,或者抛出可识别异常,让上层决定是否重试、是否人工介入。
比如,你可以加一个最简单的运行时校验函数:
// src/llm/validators.ts
import type { RequirementAnalysis } from "./schemas";
export function isRequirementAnalysis(data: unknown): data is RequirementAnalysis {
if (typeof data !== "object" || data === null) return false;
const obj = data as Record<string, unknown>;
return (
typeof obj.summary === "string" &&
Array.isArray(obj.modules) &&
Array.isArray(obj.risks) &&
Array.isArray(obj.questions)
);
}
然后在调用链路里接上:
// src/services/requirementAnalyzer.ts
import { isRequirementAnalysis } from "./validators";
async analyzeWithValidators(requirement) {
const userPrompt = buildRequirementAnalysisPrompt(requirement);
const data = await this.llm.generateJSON<RequirementAnalysis>(
REQUIREMENT_ANALYSIS_SYSTEM_PROMPT,
userPrompt,
);
if (!isRequirementAnalysis(data)) {
throw new Error("模型输出结构不符合 RequirementAnalysis 预期");
}
return data as RequirementAnalysis;
}
如果想再往前走一步,还可以给调用层留出重试位置:
// src/llm/utils.ts
async function withRetry<T>(fn: () => Promise<T>, retries = 2): Promise<T> {
let lastError: unknown;
for (let i = 0; i <= retries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
throw lastError;
}
然后在调用链路里接上:
async analyzeWithRerty(requirement) {
const userPrompt = buildRequirementAnalysisPrompt(requirement);
const data = await withRetry(async () => {
const result = await this.llm.generateJSON<unknown>(
REQUIREMENT_ANALYSIS_SYSTEM_PROMPT,
userPrompt,
);
if (!isRequirementAnalysis(result)) {
throw new Error("模型输出结构不符合 RequirementAnalysis 预期");
}
return result;
});
return data;
}
这类代码本身不复杂,但它体现了一种非常关键的工程思维:你不是在“请求一个聪明模型给你点结果”,而是在“维护一项带不确定性的系统能力”。

1.6.8 给后续扩展留接口:从“写通一次调用”到“沉淀一项能力”
这一节虽然还没有进入 LangChain、Tool Calling 或多轮状态,但模块设计最好从一开始就给后面留好接口。这样你今天写的基础调用层,明天才能自然长成更复杂的能力层,而不是推倒重来。
比如现在的 LLMClient,虽然只做了最基础的文本和 JSON 调用,但它的结构已经可以自然往后扩展:
- 后面可以增加工具参数
- 可以增加会话状态参数
- 可以增加 metadata 做日志和追踪
- 可以增加统一超时、重试、埋点
- 可以增加不同任务使用不同模型的路由策略
也就是说,这里做的并不是一次性代码,而是在给后续的工作流、工具调用和 Agent 运行时打地基。
这一节的重点只有一件事:把“一次能跑的调用”,整理成“系统里可复用的能力模块”。
因此,从 Demo 走向模块,通常需要做到:
- Prompt 不写死在业务代码里
- 有统一的模型调用层
- Prompt 可维护、可版本化
- 输出可校验、有兜底
- 结构为后续链式调用、工具调用与状态管理预留扩展点
完成这些之后,模型调用才真正从“示例代码”升级为“可复用的能力”。
下一步,我们把这层能力放回具体任务中,看看如何在不同目标下复用同一套 system/task/context/format/post-process 结构,并用约束与校验让结果稳定可接入系统。
1.7 示例:从需求摘要到代码生成
这一节的三个示例不是并列 Demo,而是一条“同一能力逐级推进”的路径:
- 示例一(需求摘要):观察 Prompt 如何影响自然语言输出的质量与稳定性(先把“说清楚”做好)
- 示例二(结构化输出):观察结构化如何让结果从“可读”变成“可被系统消费”(再把“接得住”做好)
- 示例三(代码生成):观察更强约束如何直接影响代码的可运行性与边界完整性(最后把“可直接用”做到位)
按这个顺序阅读,你会更容易看到:角色/任务/上下文/格式/后处理这些层级,并不是概念,而是在每一次升级里都在“收束输出空间”,把不确定性一步步推回可控范围。
1.7.1 从一个需求摘要开始
先看这样一段需求描述:
运营同学每天会上传一份 Excel 文件,系统需要自动解析其中的数据,并生成一份日报。日报生成后支持导出,如果文件字段缺失或格式异常,需要给出明确提示。
如果只是把这段话直接交给模型,让它“分析一下”,模型通常也能返回一段大致正确的回答。但这类回答往往粒度不稳定:有时偏产品描述,有时偏技术拆分,有时又把风险、待确认问题和实现建议混在一起。对人来说也许还能读懂,但对系统来说,这样的结果几乎无法继续使用。
因此,在真正的工程场景里,更合理的方式通常不是“直接问”,而是先把模型的角色与任务边界定义清楚。例如,可以把 system prompt 设定为“需求分析助手”,并要求模型只围绕三个部分输出:功能目标、核心模块和风险点。这样一来,模型的行为范围会明显收束,输出也更可预期。
下面是一个更接近工程使用方式的 TypeScript 示例:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: "https://api.amux.ai/v1"
});
async function summarizeRequirement() {
const response = await client.responses.create({
model: "gpt-5.2",
input: [
{
role: "system",
content: [
{
type: "input_text",
text: "你是一名产品技术分析助手,擅长将业务需求拆解为功能目标、核心模块和潜在风险。请使用简洁、清晰的中文输出。",
},
],
},
{
role: "user",
content: [
{
type: "input_text",
text: `
请分析下面这个需求,并按以下三部分输出:
1. 功能目标
2. 核心模块
3. 风险点
需求:
运营同学每天会上传一份 Excel 文件,系统需要自动解析其中的数据,并生成一份日报。日报生成后支持导出,如果文件字段缺失或格式异常,需要给出明确提示。
`.trim(),
},
],
},
],
});
console.log(response.output_text);
}
summarizeRequirement().catch(console.error);
这一版和随手提问式调用相比,最大的区别不在于代码多了几行,而在于这次调用被真正“设计”过了。模型不再只是“尽量回答”,而是被限定在一个明确的分析框架里。因此,输出通常会更集中、更清楚,也更适合作为后续处理的起点。
1.7.2 从自然语言走向结构化结果
如果说前一个例子解决的是“让模型说清楚”,那么接下来更重要的一步,就是让模型的输出能够被程序接住。
需求分析这种任务很容易让人误以为“模型说得差不多就够了”。但只要结果需要继续进入下游模块,比如传给另一个 Agent、写入数据库或触发工作流节点,问题立刻就变了:系统并不关心是不是“差不多能读懂”,系统关心的是字段是否固定、结构是否稳定、内容是否可解析。
还是同一个需求,如果把输出改成 JSON,模型返回的就不再只是一段描述,而开始变成一个结果对象。例如:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: "https://api.amux.ai/v1"
});
async function analyzeRequirementAsJSON() {
const response = await client.responses.create({
model: "gpt-5.2",
input: [
{
role: "system",
content: [
{
type: "input_text",
text: `
你是一名需求分析助手。
你必须严格按 JSON 返回。
不要输出额外解释。
如果信息不足,请不要猜测,而是在 questions 字段中提出。
`.trim(),
},
],
},
{
role: "user",
content: [
{
type: "input_text",
text: `
请分析下面这个需求,并按 JSON 返回,字段必须包括:
- summary: string
- modules: string[]
- risks: string[]
- priority: "low" | "medium" | "high"
- questions: string[]
需求:
运营同学每天会上传一份 Excel 文件,系统需要自动解析其中的数据,并生成一份日报。日报生成后支持导出,如果文件字段缺失或格式异常,需要给出明确提示。
`.trim(),
},
],
},
],
});
console.log(response.output_text);
}
analyzeRequirementAsJSON().catch(console.error);
一个典型的输出可能是:
{
"summary":"实现 Excel 文件上传、数据解析、日报生成与导出,并对异常文件提供明确提示。",
"modules": [
"文件上传与校验",
"Excel 数据解析",
"日报生成",
"日报导出",
"异常提示与处理"
],
"risks": [
"Excel 模板不统一导致解析失败",
"字段缺失影响日报生成完整性",
"大文件可能导致性能问题"
],
"priority":"high",
"questions": [
"日报导出支持哪些格式?",
"Excel 模板是否固定?"
]
}
到了这里,模型输出第一次真正开始接近“接口能力”,而不只是“聊天内容”。
summary 可以直接展示,modules 可以进入后续拆分流程,risks 可以交给风险评估逻辑,questions 可以形成需求澄清清单。结果一旦拥有稳定结构,就不再只是给人阅读,而开始具备系统可消费性。
为了让这一点更扎实,通常还需要再往前走一步:对结果做最基础的结构校验。
interface RequirementAnalysis {
summary: string;
modules: string[];
risks: string[];
priority: "low" | "medium" | "high";
questions: string[];
}
function isRequirementAnalysis(data: unknown): data is RequirementAnalysis {
if (typeof data !== "object" || data === null) return false;
const obj = data as Record<string, unknown>;
return (
typeof obj.summary === "string" &&
Array.isArray(obj.modules) &&
Array.isArray(obj.risks) &&
["low", "medium", "high"].includes(String(obj.priority)) &&
Array.isArray(obj.questions)
);
}
这一步看上去简单,却非常关键。因为从这一刻开始,模型输出不再只是“看起来像对”,而要进一步满足“结构上可被程序接受”。这也是从聊天式使用模型走向系统式接入模型的真正分界线。
1.7.3 一个简单的 AI Coding 场景
当分析类任务和结构化输出都建立起来之后,接下来就可以进入一个更贴近 AI Coding 的场景:让模型根据明确的函数需求生成一段基础代码。
先看这样一个任务:
写一个 Python 函数,接收一个用户列表,返回年龄大于 18 岁用户的邮箱地址,并处理空值情况。
这个任务看上去很简单,但它已经足够暴露很多问题。因为如果约束不清楚,模型很容易:
- 默认所有字段都存在
- 忽略空值处理
- 返回值类型模糊
- 代码能跑,但边界条件不完整
- 附带大量解释,反而不方便直接使用
如果只是非常模糊地写:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: "https://api.amux.ai/v1"
});
async function generateCode() {
const response = await client.responses.create({
model: "gpt-5.2",
input: "写一个 Python 函数,接收一个用户列表,返回年龄大于 18 岁用户的邮箱地址。",
});
console.log(response.output_text);
}
generateCode().catch(console.error);
模型当然也会生成代码,但结果的风格、边界处理和可用程度高度依赖它的临场发挥。
一旦把任务说得更清楚,结果通常会明显收束:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: "https://api.amux.ai/v1"
});
async function generatePythonFunction() {
const response = await client.responses.create({
model: "gpt-5.2",
input: [
{
role: "system",
content: [
{
type: "input_text",
text: "你是一名资深 Python 工程师,请输出简洁、可读、可直接运行的代码。",
},
],
},
{
role: "user",
content: [
{
type: "input_text",
text: `
请编写一个 Python 函数,要求如下:
1. 输入是一个用户列表,每个用户是一个字典,可能包含 name、age、email 字段
2. 返回年龄大于 18 岁用户的邮箱地址列表
3. 如果 age 为空、email 缺失或为 None,则跳过
4. 使用类型注解
5. 先简要解释思路,再输出代码
`.trim(),
},
],
},
],
});
console.log(response.output_text);
}
generatePythonFunction().catch(console.error);
一个较理想的返回结果通常会先给出简要思路,再给出一段可运行代码。到这里,读者会很自然地看到:
Prompt 的作用并不是“让模型更听话”,而是在定义一份更清晰的行为契约。输入长什么样,边界条件是什么,返回结果应该如何组织,这些越明确,代码生成的质量就越接近可用。
这个例子之所以重要,不只是因为它开始贴近 AI Coding,更因为它第一次把前面所有内容真正串了起来:角色设定、任务定义、边界约束、结构要求,最终都会直接体现在生成代码的质量上。
1.8 小结:如何让一次调用成为一项能力
如果把“智能体开发”看成一条从概念走向系统的工程路径,那么这一章想完成的事情只有一件:把大模型从“会说话的黑盒”推进到“可被系统使用的模块”。
工具调用、工作流编排、RAG 乃至多智能体协作当然都很重要,但它们的前提不是你掌握了多少框架,而是你是否先把一次模型调用设计成一个可控的接口:角色与任务边界清晰、上下文可管理、输出有明确契约、结果可解析、可校验,并且在失败时能够安全兜底。
因此,从模型调用开始,并不是降低目标,而是在打地基:**用 system/task/context/format 的分层来收束不确定性,用结构化输出把"能读懂"推进到"能接住",再用 post-process 把模型结果纳入工程闭环。**只有当这一层稳定下来,后续更复杂的能力叠加才不会放大混乱,而会沉淀为可维护、可复用、可扩展的系统能力。接下来,我们才有资格把这些“受约束的调用”组织成工作流,接入工具与状态,真正进入智能体工程的下一阶段。
写在最后🧪
这里是言萧凡的 AI 编程实验室。 我会在这里持续记录和分享 AI 工具、编程实践,以及那些值得沉淀下来的高效工作方法。 不只聊概念,也尽量分享能直接上手、能够复用的经验。 希望这间小小的实验室,能陪你一起探索、实践和成长。2026 年,一起进步。
有兴趣的话可以添加我的微信号一起交流,不仅是编程也可以是畅谈人生。