PI分析-agent 内核
我们都知道agent本质本就是一个loop,不断循环思考去解决下一步问题,经典的就是ReAct模式
这里我们重点看看pi agent内核是咋写的,依旧以prompt为入口开始看看
prompt
1 | |
我们还需分别看看AgentMessage和ImageContent是什么结构
1 | |
解析来重点看看run的过程, 上面两个prompt是重载函数,真真调用的是下面这个
1 | |
activeRun
这个activeRun是一个好设计,单独拿出来看看怎么个事
整体还是基于把promise的resolve获取出来,完成了如下功能
- 并发控制(run只运行一个人跑)
- 等待(waitForIdle)
- 中断,通过AbortController控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89type ActiveRun = {
promise: Promise<void>;
resolve: () => void;
abortController: AbortController;
};
class Agent {
private activeRun?: ActiveRun;
async run() {
if (this.activeRun) {
throw new Error("already running");
}
const abortController = new AbortController();
let resolve = () => {};
const promise = new Promise<void>((r) => (resolve = r));
this.activeRun = { promise, resolve, abortController };
try {
await this.task(abortController.signal);
console.log("done");
} catch (e) {
console.log("caught:", (e as Error).message);
} finally {
this.activeRun.resolve();
this.activeRun = undefined;
}
}
private task(signal: AbortSignal) {
return new Promise<void>((resolve, reject) => {
// 如果接受到中断信息,reject退出
const tick = setInterval(() => {
if (signal.aborted) {
clearInterval(tick);
reject(new Error("aborted"));
return;
}
console.log("working...");
}, 500);
setTimeout(() => {
clearInterval(tick);
resolve();
}, 3000);
});
}
abort() {
this.activeRun?.abortController.abort();
}
async waitForIdle() {
// 等待this.activeRun?.promise完成
await (this.activeRun?.promise ?? Promise.resolve());
}
}
async function main() {
const agent = new Agent();
const run = agent.run();
// 因为正在run,所以这里应该rejected: already running
try {
await agent.run();
} catch (e) {
console.log("rejected:", (e as Error).message);
}
// 沉睡1200,应该输出两次working...
// 然后因为调用了abort,console.log("caught:", (e as Error).message);会catch这个error,caught: aborted
setTimeout(() => agent.abort(), 1200);
await run;
// 等待run完成,所以这里应该输出idle
await agent.waitForIdle();
console.log("idle");
}
main();
// output
// rejected: already running
// working...
// working...
// caught: aborted
// idle
继续看主流程
1 | |
runAgentLoop
这个主要流程进入了agent-loop.ts
1 | |
这算是比较经典的react模式
llm输出结果 -> 调用tool -> 携带tool调用结果再去调用llm -> llm输出结果 -> 调用tool
形成这样一个循环
streamAssistantResponse
这里是与大模型通信的方法,应该会有一些细节,具体看看
1 | |
这算是整个的agent最核心的流程,我们再从全局看看,是否遗漏了什么细节
continue
1 | |
在prompt后还会有一个continue流程,我们来看看怎么个事
1 | |
再看看agent的continue是怎么个事
1 | |
我们看看runAgentLoopContinue与之前分析的runAgentLoop流程有何不用
和之前流程差不多了,除了多一个需要check最后一条消息非llm message
1 | |
工具调用
这也是比较重要的一块,看看咋写的,agent的调用tool的逻辑主要在这块
1 | |
接着就是executeToolCalls,看看具体调用流程是咋样的
1 | |
这里串行并行估计区别不大,先具体看看串行
1 | |
先看看prepareToolCall
1 | |
prepare后就可以开始调用了
1 | |
并行是类似的,只有使用了一个Promise.all用来等待所有的tool调用完成
具体的tool怎么实现,设计,我觉得需要单开一章来讲讲
全局细节
listeners
agent这个设计还算巧妙,因为pi的agent只是作为一个核心包,并不提供多余的功能,但是也需要通知外部,内部正在发生什么事情
这就设置了一个简单的发布订阅模式
1 | |
整体来讲这个agent就是loop + tool + event通知外部,设计非常简洁