
0. 🧟♂️ 引子:午夜机房的 "爆点" 预告
铜锣湾某栋写字楼的 23 楼,午夜 12 点的机房里还亮着一盏冷光。
阿星(不是那个喜剧之王,而是 Apple 开发界的 "捉鬼大师")正对着 Xcode 模拟器猛敲键盘,屏幕上Foundation Models
的日志像纸钱一样飘个不停。突然,暗恋他的阿 May 抱着笔记本电脑冲进来,声音发颤:" 星哥!上次你教我用respond
方法调用 AI 生成健康的文章,用户骂爆了!说等结果等到花都谢了,像在等僵尸变人一样慢!"

阿星推了推断了腿的眼镜,嘴角勾起一抹冷笑:" 莫慌!今天要教你的这招Streaming API
,堪称 AI 开发界的 ' 还魂丹 '------ 能让 AI 结果像贞子从电视里爬出来一样,一点一点实时冒出来!
在本篇捉妖奇谈中,您将学到如下内容:
- 🧟♂️ 引子:午夜机房的 "爆点" 预告
- 🧪 前情回顾:静态调用的 "死穴"
- 🚀 破局之招:Streaming API 的 "还魂术"
- 🕵️ 揭秘:Generable 宏的 "隐身魔法"
- 📦 收尾绝招:collect 函数的 "收魂术"
- 🎬 尾声
不过... 这招有个禁忌,用的时候千万别乱改Article
结构体的属性顺序,否则..."他突然压低声音,"AI 会生成半残的结果,比回魂夜的僵尸还吓人!:("

1. 🧪 前情回顾:静态调用的 "死穴"
要讲流式 API,得先说说之前那套 "等死你不偿命" 的老方法。
阿星打开 Playground,代码像符咒一样显现在屏幕上:
swift
import FoundationModels
import Playgrounds
// 定义文章结构体,这玩意儿就像装僵尸的棺材,格式必须严丝合缝
@Generable struct Article {
// @Guide是给AI的"引路符",告诉它每个字段该填啥
@Guide(description: "文章标题,得响亮,像道士喊的口号")
let title: String
@Guide(description: "文章正文,内容要扎实,别像空心鬼")
let body: String
@Guide(description: "相关小贴士,得实用,像给僵尸贴的镇尸符")
let tips: [String]
}
#Playground {
// 给AI的指令,相当于给僵尸下的命令
let articleGenerationInstructions = "写一篇健康相关的文章,别瞎扯"
// 建立AI会话,这步是打开阴阳两界的通道
let session = LanguageModelSession(instructions: articleGenerationInstructions)
// 调用respond方法------注意!这就是"等死法"的核心,得等AI把所有内容写完才吐出来
let response = session.respond(to: "Heart Rate", generating: Article.self)
print(response.content) // 等啊等,等AI"画完符"才能打印,用户早跑光了
}

阿星敲了敲屏幕:" 看到没?respond
方法就像请僵尸跳舞,得等它慢吞吞把整套动作做完才肯停。用户要的是 ' 实时互动 ',不是 ' 守株待兔 '!这时候,Streaming API
就要登场了 ------ 它能让 AI 像倒汽水一样,边生成边往外冒内容,简直爽到飞起!"

2. 🚀 破局之招:Streaming API 的 "还魂术"
阿 May 凑过来,眼睛瞪得像铜铃:"星哥,那这招怎么练?难不难?"
"简单到离谱!" 阿星大手一挥,代码瞬间变了样," 你看,就改一个函数,把respond
换成streamResponse
,相当于把 ' 等死符 ' 换成 ' 实时符 ',AI 立马从 ' 僵尸 ' 变' 活人 '!"
swift
import Playgrounds
#Playground {
let articleGenerationInstructions = "写一篇健康相关的文章,重点讲心率"
let session = LanguageModelSession(instructions: articleGenerationInstructions)
// 关键改动:用streamResponse替代respond,这步是打开"实时通道"的关键
// 返回的stream是AsyncSequence类型,相当于"阴阳两界的传送带",实时传结果
let stream = session.streamResponse(to: "heart rate", generating: Article.self)
// for try await循环:相当于"守着传送带捡东西",AI生成一点就拿一点
for try await article in stream {
print(article) // 这里打印的是"半成品",但用户能看到实时进度,体验直接拉满!
}
}
阿星指着代码里的AsyncSequence
,解释得唾沫横飞:" 这玩意儿可不是吃素的!它就像菜市场的传送带,AI 那边刚切好一块肉(部分结果),这边立马就能拿到。之前用respond
,得等 AI 把整头猪宰完才给你,现在是切一块给一块,用户看着文字慢慢冒出来,比看僵尸跳机械舞还过瘾!"

3. 🕵️ 揭秘:Generable 宏的 "隐身魔法"
阿 May 突然指着@Generable
问:"星哥,这玩意儿到底是啥?为啥加了它,AI 就能分阶段生成内容?"
阿星压低声音,像在说江湖秘闻:" 这@Generable
是 Apple 给开发者的 ' 隐身符 '!它会在Article
结构体里偷偷生成一个叫PartiallyGenerated
的 ' 影子结构体 '------ 你定义的title
、body
这些属性,在影子里全变成可选类型(Optional
)!"
"打个比方," 他拿起笔在纸上画了个示意图," 你定义的Article
是 ' 完整僵尸 ',有头有手有脚;而PartiallyGenerated
是 ' 半残僵尸 ',可能先有头(title),再长身体(body),最后才长腿(tips)。AI 生成的时候,就按照这个 ' 生长顺序 ' 来,绝不会乱套!"

他顿了顿,表情突然严肃:" 这里有个生死攸关的点 ------结构体属性的顺序绝对不能乱!AI 是 ' 死脑筋 ',你写 title 在前,它就先生成 title;你把 body 放前面,它就先写 body,到时候用户看到 ' 正文先冒出来,标题后蹦出来 ',会以为 APP 中了邪,直接卸载!"
4. 📦 收尾绝招:collect 函数的 "收魂术"
"那... 要是我既想要实时显示,又想要最后拿完整结果,咋办?" 阿 May 追问,像个好奇的小道士。

阿星笑了,手指在键盘上一敲,又一段代码跳了出来:" 这还不简单?用collect
函数啊!它就像 ' 收魂铃 ',等 AI 把所有内容都生成完,一摇铃就能把完整的Article
给收回来!"
swift
import Playgrounds
#Playground {
let articleGenerationInstructions = "写一篇健康相关的文章,重点讲心率"
let session = LanguageModelSession(instructions: articleGenerationInstructions)
let stream = session.streamResponse(to: "heart rate", generating: Article.self)
// 第一步:实时打印,让用户看个爽
for try await partialArticle in stream {
print("实时更新:\(partialArticle)") // 边生成边显示,用户体验拉满
}
// 第二步:用collect收完整结果,相当于"收魂"
let fullArticle = try await stream.collect()
print("完整文章:\(fullArticle.content)") // 最后拿完整版,方便后续存储、分享
}
"看到没?" 阿星得意地晃了晃脑袋," 先让用户看实时进度,过足眼瘾;等 AI 生成完,再用collect
把完整结果收回来 ------ 这招叫 ' 先礼后兵 ',既解决了等待问题,又不耽误后续操作,比道士捉鬼想得还周全!"

5. 🎬 尾声
阿 May 刚把代码跑通,屏幕上的 AI 结果就像流水一样冒了出来,她兴奋得拍着手:"星哥!这招也太好用了吧!以后再也不怕用户骂慢了!"
阿星却突然皱起眉头,盯着屏幕右下角的警告日志,声音低沉:" 别高兴太早... 这Streaming API
还有个隐藏 bug------ 如果Article
结构体里有嵌套类型,AI 生成到一半会突然 ' 卡壳 ',就像僵尸被糯米粘住一样动不了... 而且..."

他突然抬头,眼神里闪过一丝诡异:" 我听说下礼拜 Apple 要出Foundation Models 5.0
,会新增 ' 流式中断重连 ' 功能,但据说用这功能的开发者,都会收到一个神秘的推送... 里面是 AI 生成的 ' 半成品代码 ',有人说那是之前开发失败工程师的 ' 怨念 '..."
阿星把笔记本电脑合上,屏幕映出他半边脸,语气带着悬念:"想知道怎么解决嵌套类型的流式 bug 吗?想知道那个神秘推送里到底藏着什么?下礼拜同一时间,咱们接着聊 ------ 不过,下次来的时候,记得带包糯米,以防万一..."

那么,各位捉鬼的微秃小法师,你们撸码的时候也要记得"小心驶得万年船"哦!
感谢分享,下次再会啦!8-)