Angular Signals 与 Resource API:构建 AI/LLM 集成应用的响应式设计模式
2026/9/7 14:55:40 网站建设 项目流程

Angular Signals 与 Resource API:构建 AI/LLM 集成应用的响应式设计模式

【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular

本篇指南围绕 Angular 仓库中的 AI 设计模式文档 展开,讲解如何利用 signals 与resourceAPI 来应对 AI/LLM 集成中的三大难题:异步请求的触发时机、流式响应的增量展示,以及缓慢或不稳定网络下的用户体验。读完后,你将掌握“双信号触发请求”“linkedSignal累积 LLM 结果”、以及resourceloader/stream/reload()机制,并能在 Angular 应用中落地一个带加载态、错误重试与流式输出的 AI 界面。

为什么 AI/LLM 集成需要专门的设计模式

与常规确定性 API 不同,调用 AI 和大语言模型(LLM)接口会带来独特挑战:请求往往是长耗时的异步操作、响应可能是增量到达的流式数据、而且网络请求可能缓慢或失败。传统的“发起请求即阻塞”式写法难以支撑响应式的界面体验。

Angular 的 signals 和resourceAPI 恰好提供了优雅的解决手段:signals 负责细粒度、自动化的响应式状态管理,resource则把异步数据获取封装为“参数变化自动重触发、值/状态/错误均为信号”的一等公民。下面的模式均建立在这一组合之上。

用信号触发请求:分离“输入中”与“已提交”两个状态

处理用户输入的提示词(prompt)时,一个常见的问题是:如果直接把用户正在输入的信号作为请求参数,那么每敲一个键都会触发一次 LLM 调用。正确的模式是把“用户实时输入”和“触发 API 调用的提交值”分离开:

  1. 用户输入过程中,把原始输入保存在一个信号中;
  2. 用户显式提交(如点击按钮)时,把第一个信号的内容更新到第二个信号;
  3. resourceparams字段中使用第二个信号。

这样配置后,resource 的loader函数只在用户显式提交 prompt 时运行,而不是在每次击键时运行。你还可以在loader中使用额外的信号参数,例如sessionIduserId(对创建持久化的 LLM 会话很有用),这样请求始终使用这些参数的当前值,却不会因此重新触发loader中定义的异步函数。

源码印证:在 resource 的实现 中,params本质上是“重新触发计算的请求信号”——resource()params(兼容旧版request命名)作为第一个参数传给内部类ResourceImpl,当params读取的信号值发生变化时,内部基于linkedSignal的请求状态(见 状态计算逻辑)才会推进并触发加载 effect;而仅在loader中读取的其他信号(如sessionId)不参与触发判断。这正是文档所述“附加信号参数取值但不触发”的底层机制。

许多 AI SDK 都提供发起 API 调用的辅助方法。例如 Genkit 客户端库暴露了调用 Genkit flow 的runFlow方法,你可以在 resource 的loader中调用它;对于其他 API,也可以使用httpResource

下面的示例展示了一个获取 AI 生成故事片段的resource。它的loader仅在storyInput信号变化时被触发:

// A resource that fetches three parts of an AI generated story storyResource = resource({ // The default value to use before the first request or on error defaultValue: DEFAULT_STORY, // The loader is re-triggered when this signal changes params: () => this.storyInput(), // The async function to fetch data loader: ({params}): Promise<StoryData> => { // The params value is the current value of the storyInput signal const url = this.endpoint(); return runFlow({ url, input: { userInput: params, sessionId: this.storyService.sessionId(), // Read from another signal }, }); }, });

几点使用细节:

  • params() => this.storyInput()是“提交值”信号,只有它变化才会重新触发 loader;
  • loader:接收{params}上下文(即当前 params 值),在其中读取sessionId()等额外信号是安全的——它们只取当前值,不会成为触发源;
  • defaultValue:首次请求前或出错时使用的兜底值。从 resource 源码 可以看到,.value是一个 computed 信号,在流未解析时会回退到defaultValue,这保证了模板在等待期间永远有可渲染的内容。

为模板准备 LLM 数据:结构化输出与 linkedSignal 状态累积

你可以配置 LLM API 返回结构化数据。为你的resource配置与 LLM 期望输出相匹配的强类型,能获得更好的类型安全与编辑器自动补全。

管理“从 resource 派生出的状态”时,应使用computed信号或linkedSignal。因为linkedSignal可以访问先前的值,它能覆盖多种 AI 相关场景,包括:

  • 构建聊天历史(chat history);
  • 在 LLM 生成内容的同时,保留或自定义模板正在展示的数据。

在下面的示例中,storyParts是一个linkedSignal,它把storyResource返回的最新故事片段追加到已有的片段数组中:

storyParts = linkedSignal<string[], string[]>({ // The source signal that triggers the computation source: () => this.storyResource.value().storyParts, // The computation function computation: (newStoryParts, previous) => { // Get the previous value of this linkedSignal, or an empty array const existingStoryParts = previous?.value || []; // Return a new array with the old and new parts return [...existingStoryParts, ...newStoryParts]; }, });

源码印证linkedSignal的选项对象形式(source+computation)定义于 linkedSignal 实现,其中computation的第二个参数previous携带上一轮的sourcevalue,即注释中@publicApi 20.0标注的“可访问先前值的高级 API 形式”。这使得“在 source 每次更新时把新片段累积进旧数组”成为纯粹声明式的写法,无需手动维护一个可变的历史数组。值得注意的是,resource自身的内部状态管理也正是建立在linkedSignal之上的(见 ResourceImpl 构造 中“主状态由linkedSignal管理,以便请求信号变化时状态能瞬时切换”的注释),两者共享同一套响应式原语。

性能与用户体验:让慢请求不拖垮应用

LLM API 可能比传统确定性 API 更慢、更容易出错。你可以用若干 Angular 特性来构建高性能且友好的界面:

  • 作用域化的加载(Scoped Loading):把resource放在直接使用数据的组件中,有助于限制变更检测周期(在 zoneless 应用中尤为明显),并避免阻塞应用的其他部分。如果数据需要在多个组件间共享,则从服务中提供该resource
  • SSR 与 Hydration:使用带增量水合(incremental hydration)的服务端渲染(SSR)快速渲染首屏内容。可以先展示 AI 生成内容的占位符,把数据获取推迟到组件在客户端水合之后进行。
  • 加载状态:使用 resource 的LOADING状态显示指示器(如 spinner)。该状态同时覆盖首次加载与重新加载两种场景。
  • 错误处理与重试:使用 resource 的reload()方法,为用户提供简单的失败重试入口——在依赖 AI 生成内容时,失败率可能更高,这一点尤其普遍。

源码印证isLoading并非普通布尔标志,而是一个派生信号——BaseWritableResource 构造器 中它被实现为computed(() => this.status() === 'loading' || this.status() === 'reloading'),因此模板里读取imgResource.isLoading()时,状态从loading切到reloading(例如用户点了重试)依然保持为true,不会出现指示器闪烁。

下面是一个响应式 UI 示例:动态展示 AI 生成的图片,并带加载与重试功能:

<!-- Display a loading spinner while the LLM generates the image --> @if (imgResource.isLoading()) { <div class="img-placeholder"> <mat-spinner [diameter]="50" /> </div> <!-- Dynamically populates the src attribute with the generated image URL --> } @else if (imgResource.hasValue()) { <img [src]="imgResource.value()" /> <!-- Provides a retry option if the request fails --> } @else { <div class="img-placeholder" (click)="imgResource.reload()"> <mat-icon fontIcon="refresh" /> <p>Failed to load image. Click to retry.</p> </div> }

模板逻辑值得注意的分支细节:

  1. isLoading()为真 → 显示 spinner;
  2. hasValue()为真 → 直接渲染生成的图片 URL。从源码看,hasValue() 内部会先检查isError(),错误状态下即使存在旧值也不会误判为“有值”,避免展示过期数据;
  3. 其余情况(主要是错误)→ 显示可点击的重试占位,(click)="imgResource.reload()"触发重试。

实战模式:流式聊天响应

很多界面需要随着响应数据的到达而增量展示 LLM API 的部分结果。Angular 的 resource API 提供了流式响应能力以支持这类模式:resourcestream属性接受一个异步函数,你可以用它随时间对某个信号值进行多次更新;被更新的信号即代表正在流式传输的数据。

characters = resource({ stream: async () => { const data = signal<ResourceStreamItem<string>>({value: ''}); // Calls a Genkit streaming flow using the streamFlow method // exposed by the Genkit client SDK const response = streamFlow({ url: '/streamCharacters', input: 10, }); (async () => { for await (const chunk of response.stream) { data.update((prev) => { if ('value' in prev) { return {value: `${prev.value} ${chunk}`}; } else { return {error: chunk as unknown as Error}; } }); } })(); return data; }, });

characters成员被异步更新,可以直接在模板中展示:

@if (characters.isLoading()) { <p>Loading...</p> } @else if (characters.hasValue()) { <p>{{ characters.value() }}</p> } @else { <p>{{ characters.error() }}</p> }

源码印证:从 ResourceImpl 的实现 可以看到,传入stream时资源内部会为该请求维护一个stream信号(Signal<ResourceStreamItem<T>>),其状态在流未解析(!isResolved(stream))期间保持“未决”,.value.error均从该信号投影而来。因此stream函数返回的那个内部data信号每被update一次,characters.value()等公开信号就同步推进一步——这正是“逐 chunk 更新、模板自动刷新”的底层通路。

服务端部分(例如server.ts)定义发送流式数据的端点。以下代码使用 Genkit 框架配合 Gemini,但该技术同样适用于其他支持 LLM 流式响应的 API:

import {startFlowServer} from '@genkit-ai/express'; import {genkit} from 'genkit/beta'; import {googleAI, gemini20Flash} from '@genkit-ai/googleai'; const ai = genkit({plugins: [googleAI()]}); export const streamCharacters = ai.defineFlow( { name: 'streamCharacters', inputSchema: z.number(), outputSchema: z.string(), streamSchema: z.string(), }, async (count, {sendChunk}) => { const {response, stream} = ai.generateStream({ model: gemini20Flash, config: { temperature: 1, }, prompt: `Generate ${count} different RPG game characters.`, }); (async () => { for await (const chunk of stream) { sendChunk(chunk.content[0].text!); } })(); return (await response).text; }, ); startFlowServer({ flows: [streamCharacters], });

服务端要点对应客户端行为:

  • streamSchema: z.string()声明流式块为字符串,客户端stream中的chunk即按该类型逐块到达;
  • flow 函数内的sendChunk(chunk.content[0].text!)循环把模型生成的每个文本块推送给客户端,与客户端for await (const chunk of response.stream)的逐块消费一一对应;
  • return (await response).text返回完整结果作为该 flow 的最终输出(outputSchema对应),可用于日志、缓存或持久化,不影响流式展示。

适用前提:该模式需要你的应用具备服务端环境(如 Angular SSR 提供的 Node 服务)来运行 Genkit 等框架,且模型 API 支持流式生成;纯客户端方案(例如直接调用 Firebase AI Logic)可参考仓库中 AI 集成总览 中介绍的替代路线。

小结与延伸阅读

本指南给出的四个模式构成了 Angular 中 AI 集成的完整状态链路:

模式核心 API解决的问题
双信号触发signal+resource({params, loader})避免击键级触发 LLM 调用,同时动态读取会话参数
结果累积linkedSignal({source, computation})声明式构建聊天历史等派生状态
加载与重试isLoading()/hasValue()/reload()慢请求下的友好 UI 与失败恢复
流式响应resource({stream})增量渲染 LLM 逐块输出

这些模式的实现基础可以在仓库中继续深入:

  • resource实现:请求状态机、params触发、流信号管理与defaultValue回退逻辑;
  • linkedSignal实现:source/computation选项形式与“访问先前值”的 API 契约;
  • signals 总览、resource 指南、linkedSignal 指南 与 httpResource 指南:完整 API 参考与更多异步数据获取场景。

【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询