最近智谱zai推出了ClaudeCode的GLM4.5的套餐,其中Lite版仅需要每个月20元,非常便宜,本次我们就来试试通过ClaudeCode + GLM4.5来实现一个小项目。该项目fork自LangChain的DeepAgents-UI项目。
环境配置
IDE:采用windsurf
+claude code(模型为GLM4.5)
后端 :之前实现的个人版的DeepAgent(github.com/TBice123123...) 采用Langgraph官方的API 风格进行后端API提供。 环境配置如下:
bash
git clone https://github.com/TBice123123/deep-agent-langgraph.git
进入到主目录
bash
cd deep-agent-langgraph
安装依赖
bash
uv sync --dev
启动api server
bash
uv run langgraph dev --host=localhost
前端:LangChain的DeepAgents-UI项目 环境配置如下:
bash
git clone https://github.com/langchain-ai/deep-agents-ui.git
进入到主目录
bash
cd deep-agents-ui
安装依赖
bash
pnpm install
启动前端
bash
pnpm dev
一切完成好后,开始第一步,我们先让claude code读取Readme文件,然后开始分析项目。

CC给的答复是:

接下来我们让它帮我们

接下来我们启动项目看看,发个你好,结果跨域了
bash
Access to fetch at 'http://127.0.0.1:2024/threads/search' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'http://127.0.0.1' that is not equal to the supplied origin. Have the server send the header with a valid value.
于是我们把报错丢给cc,让它帮我们解决

然后我们允许其修改内容。
能够正常的回复:

当然我们可以问问他为啥要这样做

接下来我们完成一个简单的任务,实现一个温州
的旅游指南吧。 于是我们遇到了下一个问题,子智能体的没有办法输出。

这大概是前端没有处理子智能体(子图)返回的内容。 因为如下图所示

确实没有显示任何子图的输出。
我们将这个问题问一下CC。 首先让它读取项目帮我们看看前端是怎么读取SSE流的,由于这个内容太长,就直接复制cc的回答了
bash
的,下面开始读取文件,告诉我这个是怎么读取后端返
回的SSE的?
● Task(Search for SSE implementation)
⎿ Done (22 tool uses · 32.0k tokens · 1m 33.0s
● 这个应用不是使用传统的SSE(Server-Sent
Events),而是使用LangGraph
SDK的内置流式传输功能。
核心实现方式:
1. 使用LangGraph SDK的useStream Hook
// src\app\hooks\useChat.ts
const stream = useStream<StateType>({
assistantId: agentId,
client: createClient(accessToken || ""),
reconnectOnMount: true,
threadId: threadId ?? null,
onUpdateEvent: handleUpdateEvent,
onThreadId: setThreadId,
defaultHeaders: {
"x-auth-scheme": "langsmith",
},
});
2. 客户端配置
// src\lib\client.ts
export function createClient(accessToken:
string) {
const deployment = getDeployment();
return new Client({
apiUrl: deployment?.deploymentUrl || "",
apiKey: accessToken,
defaultHeaders: {
"x-auth-scheme": "langsmith",
},
});
}
3. 消息发送和流式处理
const sendMessage = useCallback(
(message: string) => {
const humanMessage: Message = {
id: uuidv4(),
type: "human",
content: message,
};
stream.submit(
{ messages: [humanMessage] },
{
optimisticValues(prev) {
const prevMessages = prev.messages ??
[];
const newMessages = [...prevMessages,
humanMessage];
return { ...prev, messages:
newMessages };
},
config: {
recursion_limit: 100,
},
},
);
},
[stream],
);
4. 事件处理
const handleUpdateEvent = useCallback(
(data: { [node: string]: Partial<StateType> })
=> {
Object.entries(data).forEach(([_, nodeData])
=> {
if (nodeData?.todos) {
onTodosUpdate(nodeData.todos);
}
if (nodeData?.files) {
onFilesUpdate(nodeData.files);
}
});
},
[onTodosUpdate, onFilesUpdate],
);
关键区别:
- 传统SSE:使用EventSource或手动fetch流式处理
- LangGraph SDK:使用封装好的useStream
Hook,SDK内部处理底层通信
SDK自动管理连接、重连、消息格式化和状态同步,开
发者只需关注业务逻辑。
我们追随CC的思路看一下这部分代码 然后很尴尬的一点,我没找到第四点位于的地方,于是我又问CC。
bash
第四点handleUpdateEvent函数在第35-47行:
const handleUpdateEvent = useCallback(
(data: { [node: string]: Partial<StateType> })
=> {
Object.entries(data).forEach(([_, nodeData])
=> {
if (nodeData?.todos) {
onTodosUpdate(nodeData.todos);
}
if (nodeData?.files) {
onFilesUpdate(nodeData.files);
}
});
},
[onTodosUpdate, onFilesUpdate],
);
这个函数在useStream配置中使用(第54行),用于处
理后端返回的更新事件,提取todos和files数据并更新
UI。
接下里我们让CC打印后端返回的所以SSE流。

虽然是打印了但是明显不是我们想要的
于是CC又改了一下:

不出意外它这次幻觉了。当然这个原因可能是它对langgraph server不了解,于是我们使用context7 mcp让其获取更多的上下文。 经过一些试错,它终于勉强的添加对了位置,但是名称写错了(最终是windsurf tab纠正了,有一定可能是因为它把python sdk的搞混了。)(不过已经很不错,换成我找估计要好久,这个没有花太多时间,大概2-3轮就基本上解决了)

子图的内容终于输出了




接下来我们让它修改左侧的Tasks和Files没有办法显示的问题 它的回答如下:
bash
● 现在我明白了!我看到在 page.tsx 中,ChatInterface
组件接收了 onTodosUpdate 和 onFilesUpdate
回调函数,这些函数应该会被 SSE 数据触发。让我查看
ChatInterface 组件是如何使用这些回调的:
● Read(src\app\components\ChatInterface\ChatInterface.tsx)
⎿ Read 50 lines (ctrl+r to expand)
● Read(src\app\components\ChatInterface\ChatInterface.tsx)
⎿ Read 30 lines (ctrl+r to expand)
● 现在我理解了数据流。让我创建一个计划来分析并修复 Tasks
和 Files 的更新问题:
● 现在我来分析 SSE 数据结构和处理逻辑。让我重新检查
useChat.ts 中的 handleUpdateEvent 函数:
● Read(src\app\hooks\useChat.ts)
⎿ Read 20 lines (ctrl+r to expand)
● 现在让我查看官方示例中如何处理这些数据更新:
● context7 - get-library-docs (MCP)(context7CompatibleLibrary
ID:
"/langchain-ai/langgraph",
topic: "useStream
onUpdateEvent todos files
example", tokens: 10000)
⎿ ⚠ Large MCP response (~13.6k tokens), this can fill
⎿ ========================
CODE SNIPPETS
========================
... +2121 lines (ctrl+r to expand)
● 基于我的分析,问题在于数据处理逻辑。现在让我修复
useChat.ts 中的 handleUpdateEvent 函数,使其能正确处理
SSE 数据并更新 Tasks 和 Files:
● Update(src\app\hooks\useChat.ts)
⎿ Updated src\app\hooks\useChat.ts with 30 additions
and 3 removals
35 const handleUpdateEvent = useCallback(
36 (data: { [node: string]:
Partial<StateType> }) => {
37 console.log("SSE流数据:", data);
38 - Object.entries(data).forEach(([_,
- nodeData]) => {
39 - if (nodeData?.todos) {
38 +
39 + // 遍历所有节点数据
40 +
+ Object.entries(data).forEach(([nodeName,
+ nodeData]) => {
41 + console.log(`节点 ${nodeName}
+ 数据:`, nodeData);
42 +
43 + // 处理 todos 数据
44 + if (nodeData?.todos &&
+ Array.isArray(nodeData.todos)) {
45 + console.log(`发现 todos
+ 数据,数量: ${nodeData.todos.length}`);
46 onTodosUpdate(nodeData.todos);
47 }
48 - if (nodeData?.files) {
48 +
49 + // 处理 files 数据
50 + if (nodeData?.files && typeof
+ nodeData.files === 'object') {
51 + console.log(`发现 files
+ 数据,键数量:
+ ${Object.keys(nodeData.files).length}`);
52 onFilesUpdate(nodeData.files);
53 }
54 +
55 + // 处理子图数据 -
+ 子图数据可能嵌套在更深的结构中
56 + if (nodeData && typeof nodeData
+ === 'object') {
57 + // 检查是否有嵌套的子图数据结构
58 +
+ Object.values(nodeData).forEach((value:
+ any) => {
59 + if (value && typeof value ===
+ 'object') {
60 + // 检查是否有 todos 或
+ files
61 + if (value?.todos &&
+ Array.isArray(value.todos)) {
62 + console.log(`发现嵌套
+ todos 数据,数量: ${value.todos.length}`);
63 +
+ onTodosUpdate(value.todos);
64 + }
65 + if (value?.files && typeof
+ value.files === 'object') {
66 + console.log(`发现嵌套
+ files 数据,键数量:
+ ${Object.keys(value.files).length}`);
67 +
+ onFilesUpdate(value.files);
68 + }
69 + }
70 + });
71 + }
72 });
73 },
74 [onTodosUpdate, onFilesUpdate],
● 已经修复了数据处理逻辑!现在 handleUpdateEvent
函数能够:
1. 详细日志记录:记录 SSE
流数据和每个节点的数据,便于调试
2. 类型安全检查:确保 todos 是数组类型,files
是对象类型
3. 嵌套数据处理:支持处理子图数据中可能存在的嵌套结构
4. 全面的数据扫描:遍历所有可能的嵌套结构来查找 todos
和 files 数据
主要改进点:
- 添加了详细的 console.log 来帮助调试 SSE 数据结构
- 增加了类型检查 (Array.isArray 和 typeof) 防止错误更新
- 支持递归查找嵌套的 todos 和 files 数据
- 保持了原有的子图流式传输配置 (streamSubgraphs: true)
现在 Tasks 和 Files 选项卡应该能够正确显示从 SSE
返回的数据了。你可以在浏览器控制台中查看详细的日志,了
解数据是如何被处理和更新的。
将其给的代码应用后,控制台可以正常捕获tools的数据

我们把这个有用的内容给到CC并提示它只有tools节点才会更新todo 顺利更新


当然这里有点问题不过这个不是cc的问题是因为我这里和官方不一样完成的是done。
bash
> 可以实现todo的更新,但是还是有问题,这里可能是我的问题我的实现和官
方不太一样,我的todo完成的时候是done,官方的是compeleted。请帮我改
一下前端代码?
● Read(src\app\components\TasksFilesSidebar\TasksFilesSidebar.tsx)
⎿ Read 173 lines (ctrl+r to expand)
● Update(src\app\components\TasksFilesSidebar\TasksFilesSidebar.tsx)
⎿ Updated
src\app\components\TasksFilesSidebar\TasksFilesSidebar.tsx
with 2 additions and 2 removals
29 ({ todos, files, onFileClick, collapsed,
onToggleCollapse }) => {
30 const getStatusIcon = useCallback((status:
TodoItem["status"]) => {
31 switch (status) {
32 - case "completed":
32 + case "done":
33 return <CheckCircle size={16}
className={styles.completedIcon} />;
34 case "in_progress":
35 return <Clock size={16}
className={styles.progressIcon} />;
...
42 return {
43 pending: todos.filter((t) => t.status ===
"pending"),
44 in_progress: todos.filter((t) => t.status
=== "in_progress"),
45 - completed: todos.filter((t) => t.status ===
- "completed"),
45 + completed: todos.filter((t) => t.status ===
+ "done"),
46 };
47 }, [todos]);
48
● 已经修改完成。现在前端的 TasksFilesSidebar 组件会正确处理你的
"done" 状态:
1. getStatusIcon 函数现在会将 "done" 状态显示为完成图标
2. groupedTodos 现在会将状态为 "done" 的 todo 项目归类到已完成组
这样你的应用就能正确显示 todo 项目的完成状态了。
我们惊喜的发现它确实改的是正确的。

bash
看到 write_note 节点输出的数据结构了。现在需要修改 useChat.ts
来处理 write_note 节点中的文件数据。
● Update(src\app\hooks\useChat.ts
)
⎿ Updated
src\app\hooks\useChat.ts
with 12 additions and 1
removal
36 (data: {
[node:
string]: Parti
al<StateType>
}) => {
37
console.log("S
SE流数据:",
data);
38
39 - // 专门
- 处理 tools
- 节点的数据
39 + //
+ 处理 tools
+ 节点的数据
40 if
(data.tools) {
41 conso
le.log("tools
节点数据:",
data.tools);
42
...
52
onFilesUpdate(
data.tools.fil
es);
53 }
54 }
55 +
56 + // 处理
+ write_note
+ 节点的数据
57 + if (dat
+ a.write_note)
+ {
58 +
+ console.log("w
+ rite_note
+ 节点数据:",
+ data.write_not
+ e);
59 +
60 + //
+ 处理 note 数据
61 + if
+ (data.write_no
+ te?.note &&
+ typeof data.wr
+ ite_note.note
+ === 'object')
+ {
62 + con
+ sole.log(`发现
+ note
+ 数据,键数量:
+ ${Object.keys(
+ data.write_not
+ e.note).length
+ }`);
63 +
+ onFilesUpdate(
+ data.write_not
+ e.note);
64 + }
65 + }
66 },
67 [onTodosU
pdate, onFiles
Update],
68 );
● 已经修改完成。现在代码会同时处理:
1. tools 节点:处理 todo 数组和 files 对象
2. write_note 节点:处理 note
对象(包含温州旅游攻略和江心屿上午行程等文件)
根据你提供的控制台输出,write_note 节点的 note
对象包含了两个文件:
- 温州旅游攻略
- 温州江心屿上午行程
这些文件现在应该会正确显示在 Files 标签页中。
这一次cc改的也是对的。
跟官方的效果基本上一样。


至此官方的deepagent-ui魔改完成。