Skip to content

Commit 4a0a62a

Browse files
committed
fix: truely streaming
1 parent 16dfcfa commit 4a0a62a

File tree

4 files changed

+74
-39
lines changed

4 files changed

+74
-39
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@ temp
6060
# claude code
6161
.claude/
6262
.serena/
63+
CLAUDE.md

examples/serve-astack/backend/src/routes/chat.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export default async function chatRoutes(fastify: FastifyInstance) {
121121
reply.header('X-Vercel-AI-Data-Stream', 'v1');
122122

123123
let fullContent = '';
124+
let lastSentLength = 0; // 🎯 追踪已发送的内容长度
124125

125126
try {
126127
// Stream the agent processing using AI SDK Data Stream Protocol
@@ -144,37 +145,38 @@ export default async function chatRoutes(fastify: FastifyInstance) {
144145

145146
case 'assistant_message': {
146147
if (chunk.content) {
148+
// 🎯 真流式修复:现在 chunk.content 已经是实时incremental的了
149+
// 不需要二次分割,直接发送即可获得最佳流式体验
150+
147151
fullContent = chunk.content;
148152

149-
// 智能流式传输:针对代码内容优化
150-
let chunks: string[];
151-
if (STREAMING_CONFIG.streamByCharacter) {
152-
// 字符级流式
153-
chunks = chunk.content.split('');
154-
} else {
155-
// 智能分块:对于包含代码的内容,使用更细粒度的分割
156-
if (
157-
chunk.content.includes('```') ||
158-
chunk.content.includes('def ') ||
159-
chunk.content.includes('function ') ||
160-
chunk.content.includes('class ')
161-
) {
162-
// 代码内容:按行分割,保持良好的流式效果
163-
chunks = chunk.content.split(/(\n)/);
164-
} else {
165-
// 普通文本:按词语分割
166-
chunks = chunk.content.split(/(\s+)/);
167-
}
168-
}
153+
// 获取新增的内容(incremental delta)
154+
const newContent = fullContent.slice(lastSentLength);
155+
lastSentLength = fullContent.length;
169156

170-
for (const textChunk of chunks) {
171-
if (textChunk) {
172-
// 跳过空字符串
173-
// Text Part: 0:string\n
174-
const textPart = `0:${JSON.stringify(textChunk)}\n`;
157+
if (newContent) {
158+
// 🔧 保持配置化的流式传输选项
159+
if (STREAMING_CONFIG.streamByCharacter) {
160+
// 字符级流式:对新增内容进行字符分割
161+
const chars = newContent.split('');
162+
for (const char of chars) {
163+
if (char) {
164+
const textPart = `0:${JSON.stringify(char)}\n`;
165+
reply.raw.write(textPart);
166+
167+
if (STREAMING_CONFIG.delayPerToken > 0) {
168+
await new Promise(resolve =>
169+
setTimeout(resolve, STREAMING_CONFIG.delayPerToken)
170+
);
171+
}
172+
}
173+
}
174+
} else {
175+
// 直接发送增量内容(推荐,性能最佳)
176+
const textPart = `0:${JSON.stringify(newContent)}\n`;
175177
reply.raw.write(textPart);
176178

177-
// 可配置的延迟
179+
// 可选的小延迟(现在主要用于视觉效果)
178180
if (STREAMING_CONFIG.delayPerToken > 0) {
179181
await new Promise(resolve =>
180182
setTimeout(resolve, STREAMING_CONFIG.delayPerToken)

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@astack-tech/components",
3-
"version": "0.1.1-beta.1",
3+
"version": "0.1.1-beta.2",
44
"description": "Components for the Astack AI Framework.",
55
"main": "dist/index.cjs",
66
"module": "dist/index.js",

packages/components/src/agents/StreamingAgent.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,13 @@ export class StreamingAgent extends Component {
186186
const tools = (this.agent as unknown as { tools?: unknown[] }).tools || [];
187187
const model = (
188188
this.agent as unknown as {
189-
model: { chatCompletion: (...args: unknown[]) => Promise<unknown> };
189+
model: {
190+
chatCompletion: (...args: unknown[]) => Promise<unknown>;
191+
streamChatCompletion: (
192+
messages: Message[],
193+
options?: { temporaryTools?: unknown[] }
194+
) => AsyncGenerator<Partial<MessageWithToolCalls>>;
195+
};
190196
}
191197
).model;
192198
const verbose = (this.agent as unknown as { verbose?: boolean }).verbose || false;
@@ -214,28 +220,54 @@ export class StreamingAgent extends Component {
214220
type: 'model_thinking',
215221
};
216222

217-
// 调用模型获取回复(与原 Agent 完全一致
218-
const modelResponse = (await model.chatCompletion(currentMessages, {
223+
// 调用模型获取流式回复(真正的SSE流式
224+
const streamGenerator = model.streamChatCompletion(currentMessages, {
219225
temporaryTools: tools,
220-
})) as MessageWithToolCalls;
226+
});
227+
228+
let accumulatedContent = '';
229+
let finalToolCalls: MessageWithToolCalls['tool_calls'] = [];
230+
const modelResponse: MessageWithToolCalls = {
231+
role: 'assistant',
232+
content: '',
233+
tool_calls: [],
234+
};
235+
236+
// 处理真正的流式响应
237+
for await (const chunk of streamGenerator) {
238+
if (chunk.content) {
239+
accumulatedContent += chunk.content;
240+
modelResponse.content = accumulatedContent;
241+
242+
if (verbose) {
243+
console.log('[StreamingAgent Debug] 收到流式chunk:', chunk.content);
244+
}
245+
246+
// 实时流式输出每个 content chunk
247+
yield {
248+
type: 'assistant_message',
249+
content: accumulatedContent,
250+
toolCalls: finalToolCalls,
251+
};
252+
}
253+
254+
// 处理工具调用(通常在流式结束时到达)
255+
if (chunk.tool_calls) {
256+
finalToolCalls = chunk.tool_calls;
257+
modelResponse.tool_calls = finalToolCalls;
258+
}
259+
}
221260

222261
if (verbose) {
223262
console.log(
224-
'[StreamingAgent Debug] 收到模型回复:',
263+
'[StreamingAgent Debug] 流式完成,最终回复:',
225264
JSON.stringify(modelResponse, null, 2)
226265
);
227266
}
228267

229268
// 保存模型回复作为最后的助手消息
230269
lastAssistantMessage = modelResponse;
231270

232-
// 流式输出:助手消息
233-
yield {
234-
type: 'assistant_message',
235-
content: modelResponse.content || '',
236-
toolCalls: modelResponse.tool_calls,
237-
};
238-
239271
// 检查是否有工具调用(与原 Agent 逻辑一致)
240272
if (!modelResponse.tool_calls || modelResponse.tool_calls.length === 0) {
241273
if (verbose) {

0 commit comments

Comments
 (0)