Agent应用开发和渲染之间关系的思考

解决问题:Agent应用开发和自定义渲染之间的关系

适合人群:'传统开发' 转 'Agent开发'过程中,对数据和UI渲染之间关系有困惑的人

心智模型:

  1. 理解LLM的定位和功能
  2. 理解客户端和服务端之间的协议定位
  3. 数据产生和渲染处理

我理解的 Agent 应用开发的核心在于:借助 LLM(大语言模型) 去驱动工程。

所以关键还是要借助 LLM 去驱动这个渲染关系。

如何做呢?带着心智模型,听我道来:


1. 理解LLM的定位和功能

理解,推理和生成

LLM 能做一件什么事呢?

最简单的例子就是:llm.invoke('你好'),然后它会回复一句:

复制代码
你好呀,请问有什么能够帮助你?

这个过程,它会做:

  1. 理解
  2. 推理
  3. 生成

现在我们知道 LLM 可以理解、推理和生成自然语言文本。


Function Call

LLM 本质是文本续写。工具调用并不是模型真的"执行"了函数,而是模型在训练后学会了在特定格式下输出一段结构化文本(JSON),表达对外部工具的调用意图。

在模型被训练出来之前,业界靠 Prompt 工程 :让模型输出 特定格式,再用正则等方法去抠。能用,但很脆------格式动不动就跑偏、参数瞎编。(之前实习过程中就使用过 Guidance 做过类似的约束性Prompt工程)

现在让 LLM 做约束性的回答变得简单了,因为很多 LLM 厂商训练过的模型大部分都可以支持这种能力,例如DeepSeek:api-docs.deepseek.com/zh-cn/guide...

现在我们知道 LLM 通过 Function Call 实现了从"单纯的文本上的理解/推理/生成",到具备可以去描述我们组织的一些 Tools(我们写的一些方法定义)。

2. 理解客户端和服务端之间的协议定位

SSE

SSE(Server‑Sent Events,服务器发送事件)是一种基于标准 HTTP 的单向实时推送技术,由 W3C 标准化,允许服务器通过 text/event-stream 长连接持续向客户端发送数据,常用于实时通知、行情推送和 AI 流式输出等场景。

SSE 只是为流式内容提供了一套标准事件格式和消费约定:

vbnet 复制代码
event: progress        ← 事件类型
id: 12                 ← 事件标识,可用于重连游标
retry: 3000            ← 重连等待时间,毫秒
data: {"percent":60}   ← 事件数据

                       ← 空行结束一个事件

对应的Accept头部:text/event-stream,也可以不使用它,自己定义格式消费实现流式传输。

因为Agent应用天然是"服务器发送事件",而且加上LLM输出是一件耗时的事情,所以SSE协议非常合适做这一类应用的协议。

浏览器有一个实现方式EventSource,但事实上,很少会采用它去做这件事,从MDN的示例上来看:developer.mozilla.org/zh-CN/docs/... 会发现问题:

  1. new EventSource(url, configuration); 只针对资源路径,无法带着内容。那我想说一句:"你好",可怎么带着?
  2. 在介绍中说的很清楚:EventSource 实例会对 HTTP 服务器开启一个持久化的连接,它是一个严格单向的协议。好家伙,严格单向,那我们通常的使用习惯是:我输入了一句话,然后等待服务器不断输出这样的形式。而这缺了一步。该怎么做呢?

通常做法是使用HTTP请求作为第一次请求,方法使用POST,并且在Http Accept头部指定:text/event-stream,这样又能带着消息,文件,图片等内容。服务器又明白通过text/event-stream的方式去处理。

javascript 复制代码
const response = await fetch("/api/chat", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "text/event-stream",
  },
  body: JSON.stringify({ message: "你好" }),
});

// 后续从 response.body 逐步读取字节,
// 经过 UTF-8 解码和 SSE 解析,再交给界面。

这里有一个开源项目可以学习这种做法:github.com/Azure/fetch...

javascript 复制代码
import { fetchEventSource } from "@microsoft/fetch-event-source";

async function sendMessage(message, render) {
  const controller = new AbortController();
  let answer = "";
  let completed = false;

  await fetchEventSource("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message }),
    signal: controller.signal,

    // 此例不因页面切到后台而主动关闭流。
    openWhenHidden: true,

    async onopen(response) {
      const type = response.headers.get("content-type") ?? "";
      if (!response.ok || !type.startsWith("text/event-stream")) {
        throw new Error(`无法开始接收:HTTP ${response.status}`);
      }
    },

    onmessage(event) {
      const data = JSON.parse(event.data);

      if (event.event === "delta") {
        answer += data.text;
        render(answer);
      }

      if (event.event === "done") {
        completed = true;
        controller.abort();
      }
    },

    onclose() {
      if (!completed) {
        throw new Error("响应提前中断");
      }
    },

    onerror(error) {
      // 本例禁止自动重发 POST,避免重新触发一次生成。
      throw error;
    },
  });
}

await sendMessage("帮我分析这份报告", (text) => {
  document.querySelector("#answer").textContent = text;
});

这里以SSE为例学习两端协议的思想。text/event-stream是一种可学习的方式,按照对这种格式的理解,我们也可以自定义开发中的协议。

css 复制代码
Content-Type: application/x-ndjson

{"type":"progress","percent":20}
{"type":"progress","percent":60}
{"type":"completed","result":"分析完成"}

3. 数据产生和渲染处理

要让后端或模型驱动前端组件,首先需要让它们知道:有哪些组件、分别适合什么场景、接收什么参数,以及用户操作后会产生什么事件。

以 React 中一个简化的审批组件为例:

javascript 复制代码
interface ApprovalProps {
  /** 审批请求的唯一标识 */
  requestId: string
  /** 需要用户确认的操作 */
  title: string
  /** 操作可能产生的影响 */
  impact: string
  /** 用户作出审批决定 */
  onResolve: (decision: 'approved' | 'rejected') => void
}

export function Approval({
  title,
  impact,
  onResolve,
}: ApprovalProps) {
  return (
    <section>
      <h3>{title}</h3>
      <p>{impact}</p>

      <button onClick={() => onResolve('approved')}>
        批准
      </button>
      <button onClick={() => onResolve('rejected')}>
        拒绝
      </button>
    </section>
  )
}

这段代码已经定义了数据结构和交互行为。但仅凭 title: string,模型无法知道这个组件适合什么场景;仅凭 onResolve,后端也不知道应该接收什么事件。

因此,还需要在组件旁边声明用途和动作映射。下面的 approvalMeta 是本方案约定的元信息格式:

javascript 复制代码
export const approvalMeta = {
  name: 'Approval',
  description: '展示一项操作及其影响,请求用户批准或拒绝。',
  useWhen: ['业务流程需要人工确认后才能继续'],
  avoidWhen: ['操作已经完成,只需要展示结果'],
  events: {
    onResolve: {
      type: 'approval.resolve',
      targetFrom: 'requestId',
      payloadFromArgs: ['decision'],
    },
  },
} as const

这里的映射表示:调用 onResolve(decision) 时,以 requestId 标识审批对象,将参数放入 payload.decision,向外发出 approval.resolve 事件。

开发者维护组件类型、说明和动作映射,构建工具负责将它们整理成统一的组件契约。

构建时,工具读取 ApprovalProps 的类型与字段注释,再合并 approvalMeta,生成类似下面的机器可读描述:

json 复制代码
{
  "name": "Approval",
  "description": "展示一项操作及其影响,请求用户批准或拒绝。",
  "useWhen": ["业务流程需要人工确认后才能继续"],
  "avoidWhen": ["操作已经完成,只需要展示结果"],
  "propsSchema": {
    "type": "object",
    "properties": {
      "requestId": {
        "type": "string",
        "description": "审批请求的唯一标识"
      },
      "title": {
        "type": "string",
        "description": "需要用户确认的操作"
      },
      "impact": {
        "type": "string",
        "description": "操作可能产生的影响"
      }
    },
    "required": ["requestId", "title", "impact"],
    "additionalProperties": false
  },
  "events": {
    "onResolve": {
      "type": "approval.resolve",
      "targetFrom": "requestId",
      "payloadFromArgs": ["decision"],
      "payloadSchema": {
        "type": "object",
        "properties": {
          "decision": {
            "enum": ["approved", "rejected"]
          }
        },
        "required": ["decision"],
        "additionalProperties": false
      }
    }
  }
}

onResolve 是函数,不能直接作为 JSON 参数传输,所以它被表达为事件契约。前端在渲染时负责绑定实际回调。

这个提取过程需要额外的构建工具实现,普通 TypeScript 编译不会自动生成上述描述。工具也需要明确支持的类型范围:字符串、对象、数组等可以转换;ReactNode、渲染函数等类型则需要适配或排除。

多个组件的契约汇总后,就形成了组件目录(Catalog)。目录可以随构建产物发布,也可以由独立服务提供给后端和模型。描述文字仍由开发者编写,构建工具自动完成的是提取、转换和汇总。

有了目录,业务数据就可以被转换成具体的渲染指令。例如,后端产生一笔待审批请求,UI 决策层选择 Approval,输出:

json 复制代码
{
  "id": "approval-view-001",
  "component": "Approval",
  "props": {
    "requestId": "request-001",
    "title": "是否发布本次更新?",
    "impact": "发布后,所有用户将使用新版本。"
  }
}

业务数据由后端提供。UI 决策层可以通过确定规则选择组件,也可以让模型根据目录中的说明选择组件。对于"待审批请求展示审批卡片"这种固定关系,直接使用规则即可。

渲染指令通过 SSE 等方式传到前端后,前端依次完成:

  1. 根据 component 查找本地注册的组件。
  2. 使用对应的 propsSchema 校验参数。
  3. 根据事件契约绑定回调,将参数传入实际组件。

因此,"component": "Approval" 最终对应的是前端开发者编写的 <Approval />。组件的布局、颜色和交互样式仍由本地实现决定。未知组件或非法参数应进入明确的错误或降级路径。

当用户点击"批准"时,绑定的回调再将操作转换为结构化事件:

json 复制代码
{
  "type": "approval.resolve",
  "targetId": "request-001",
  "payload": {
    "decision": "approved"
  }
}

后端接收事件,检查当前审批状态与用户权限,执行业务处理,再将更新后的状态发送给前端。这样,组件声明、渲染指令和用户操作就连接成了完整的数据流;同一份组件目录同时服务于模型选型、前端校验和事件绑定。


以上就是作者对于Agent应用开发和渲染之间关系的思考。欢迎讨论。

相关推荐
智能RPA1 小时前
能源电力行业智能体自动化平台对比评测(调度与抄表场景)
人工智能·自动化·能源·agent·rpa
沛东的认知和实践1 小时前
LLM-Wiki总论:把知识库编译成 Wiki,再把检索权交给 LLM——彻底搞懂LLM Wiki第一篇
agent
去伪存真2 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent
墨心@3 小时前
第 4 章《工具》学习总结
学习·自然语言处理·prompt·agent·harness
七夜zippoe3 小时前
WorkBuddy Agent + Blender 5.2 bpy 程序化建模实战:12 轮迭代建一座冬日微缩展示
agent·blender·建模·workbuddy·程序化实战
智能RPA4 小时前
农业与矿业行业智能体自动化平台对比评测(计量与巡检场景)
运维·人工智能·python·自动化·agent·rpa
user4465117917914 小时前
AgentScope 2.0 框架技术解析
agent
sg_knight4 小时前
ZCode Bug 定位实战:把报错丢给 ZCode,它是怎么修的
llm·bug·agent·ai编程·glm·智谱·zcode
宋哥转AI4 小时前
AgentScope Java 实战 04:互通层——A2A 协作与 Nacos 接线
人工智能·agent·ai编程