CANOE

この長い長い旅の その意味を 君に届けたい

最近读了《Agentic Design Patterns》。之前做过agent相关的项目,所以书里的很多想法都或多或少接触过。这本书挺适合没有任何背景的人了解一下Agent和AI相关的技术,感觉非常全面。

一些感受就是Agent可能是编程语言的未来形式,或者说编程语言之上的一层抽象,以前我们用程序做一件事,我们可能需要用编程语言描述它,但现在我们可以完全使用自然语言来描述。通过自然语言的描述,LLM就能理解要做什么,那相比程序化的写法有什么好处呢?一是用户的input可以就是自然语言,可以比用户直接call某个特定的api包含更多信息,且更灵活(因为用户描述的自然语言可能需要灵活地call多个api来实现),那用户自然可以自己call这些api达到相同的效果,但是多了一层LLM后,就可以做到语言就是实现;二是模型可以经过微调适应更复杂多变、难以描述成程序语言的逻辑,比如说分析error log并做出下一步处理,或者根据特定标准评价一段代码。

所以基于当前LLM的推理速度,只有描述成编程语言后过于复杂的任务,才适合用LLM处理。而剩下的任务还是由传统代码实现(比如用MCP或tool的方式)。LLM可以处理几乎任何任务,但带来的问题就是:一可能存在幻觉和不确定,二比传统逻辑更大的开销(token和时间)。

下面记录一些我觉得比较有趣的pattern:

  • Routing pattern: routing pattern就像编程语言里的if,但是通过LLM来做branching。如果以后LLM的推理足够快,会不会LLM也会成为像if语言一样标准化的语句?比如在程序执行的过程中,有一个指令就是 llm “一些逻辑”,然后代码会自动调用llm,根据当前程序运行的复杂状态判断branch到哪个位置。

  • Learning and adaption pattern: 会不会LLM会在运行中自主修改自己的代码,去适应更加复杂多变的情况?或者就直接编写代码自主执行?这可能比较类似现在的Planning pattern,planning是自主确定某个有限制的行动序列,但未来的adaption可能会是自我修改代码。但可能有的问题是需要有办法去validate LLM对自己代码的修改。

  • Memory management: 有了memory以后,agent能记住更多东西,就像一个人一样,它会拥有chat history以外的context。其实也和普通程序一样,比如我们会记录一些用户数据来让应用更符合用户偏好,LLM有了memory也可以达到相同的效果。

  • Goal setting pattern: 这有点像强化学习,它让agent生成结果多了一个evaluation的步骤,就像reward function,只有当reward 达到特定的threshold,才会输出执行。感觉很适合和adaption pattern做结合,比如adaption的结果达到特定的goal,再执行下一步。

  • Inter-Agent communication pattern (A2A): 感觉和MCP比较类似,MCP把传统软件/API做了抽象,而A2A把Agent拿出来单独做了层抽象,可以像call MCP一样调用别的agent。

最近参加了公司的AI Project开发活动,项目链接在这里:Kotori。总共大概花了30h左右时间,从接触熟悉LangGraph到最后的调试。简单地记录一下。

Kotori是小鸟的名字,也是我Rewrite里面最喜欢的角色。所以在写这个项目的时候一直很有动力,因为会想到小鸟线的故事,就感觉不停地有新的想法产生。

一开始在构思项目的时候想做角色扮演类的AI,所以我找了Rewrite所有的文本想做finetune,但中途又看到了LangGraph的框架,觉得很有意思,就想试试,于是把目标修改成帮助日语学习。

因为我平时经常用日语和AI聊天,我觉得AI在帮助修改语言方面绝对是强项,对语言学习者来说,绝对是这个时代最大的红利之一。我一直是自学日语,学了好多年了。但是,除了去日本旅游期间,我基本上没有和任何人有过日语的对话,所以就导致口语/写作一直是我日语学习的短板。因此,我更想让Kotori帮助我进行日语输出的练习。

Kotori融合了我觉得语言学习中比较重要的元素:

1)试错:和Kotori聊天的过程,可以用任意的方式进行表达,但即便我的语言是不通的,她也基本能理解,会继续对话;同时,会给我建议更好的表达。
2)复盘:其实就是把不会的单词记录下来进行一个复习,毕竟人类的记忆就是通过重复加强的。

我想更模拟母语学习者学习语言的过程,就是在日常生活和对话中碰到不会的表达,通过询问AI得到解释;然后通过多次的复习得到巩固。虽然感觉最后给的两种模式,还是比较简单,还是欠缺了一些考虑,但也能达到不错的效果了。整个开发的过程也很开心,虽然碰到了一些LangGraph的问题,但都一点点想办法解决了,也通过她熟悉了很多LangGraph的特性。

以下是幻想时间,如果有时间的话,想看看能不能实现:

1)Kotori会更像一个人,她拥有和聊天者的记忆,不会随session的结束而消失。
2)将Finetune过的Kotori接入Kotori Agent的聊天模式,帮助日语学习。
3)Kotori可以看到用户的屏幕,由此获得聊天话题。

(果然我的最终幻想就是在真实世界和纸片人对话😀)

Kotori的屏幕截图

最近看了Crafting Interpreters 这本书, 准备记录一下其中一个challenge的实现过程。是challenge 3,关于在clox中实现BETA。

Overview

In BETA semantics, method calls start at the top of the class hierarchy and walks downwards. A superclass method takes precedence over a subclass method. The inner keyword is introduced to allow superclass methods to call the overridden method in the subclass. An example looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Doughnut {
cook() {
print "Fry until golden brown.";
inner();
print "Place in a nice box.";
}
}

class BostonCream < Doughnut {
cook() {
print "Pipe full of custard and coat with chocolate.";
}
}

BostonCream().cook();

What could be challenging?

Before implementing BETA semantics, when we call a method, we just look up the method in the class’s method table and that’s it. However, for BETA, we need to climb up the class inheritance tree to the top to see if we could get the method in any superclass. And that would be our starting point for calling the method.

What makes the story worse is that we could not throw the inner method away. We need to store it somewhere in the stack, so that when the top-level method calls inner(), we will be able to find it.

Where to put the inner method?

When capturing a method, we also captured this using ObjBoundMethod. For inner, we could also store it in ObjBoundMethod, so that we have a way to store all the inner methods (till the innermost one) in the stack.


graph TD
A["top level method calls inner()"] --"look up inner in the stack"--> B["ObjBoundMethod A (inner's method)"] --"read A.inner, place that in the stack"--> C["ObjBoundMethod B (inner's inner)"]

The new ObjBoundMethod looks like this (the fourth line is added):

1
2
3
4
5
6
typedef struct {
Obj obj;
Value receiver; // this is actually a ObjInstance
Value inner; // this is actually an ObjBoundMethod / ObjNative
ObjClosure* method; // if this is nil, calling a native method (will be explained in Implementation session)
} ObjBoundMethod;

If a method does not have inner method, we could set its inner to be ObjNative nil, which will return nil.

We need to have a place to put inner in the stack. It could be the same as how we put this in the stack. If say BostonCream inherits Doughnut, the call stack for Doughnut cook will looks like this:


graph LR
A["(Doughnut cook call frame) this"]--> B["inner(BostonCream's cook())"] -->C["arg 0"] --> D["arg 1..."]

Implementation

When calling invokeFromClass:

  1. Get method from class’s method table
  2. If class has super
    • if current class has the method, build a ObjBoundMethod and stored it in the stack
    • if current class does not have the method, which means inner is nil, put a nil method in the stack
    • Note that if inner is not nil method, we still need to build a ObjBoundMethod in the stack, so that when GC is happening, inner is not lost (later, we need to recursively call invokeFromClass to reach the top-level class of the inheritance tree. It is possible that the top-level class does not implement the method we want to call. In that case, we should make sure the most-top-level class that implement the method does not get lost)
  3. call invokeFromClass using super
    • if call is successful, just return true
    • Otherwise, reset the inner to the original inner (this is possible as we enforced 2b.i.)
  4. If in step 1. we get the method from current class method table, call that method
    • otherwise, return false, indicating calling the method is not successful.
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
// Stack: receiver, inner, args...
static bool invokeFromClass(ObjClass* klass, ObjString* name,
int argCount, Value* inner) {
assert(inner != NULL);

Value method;
Value receiver = peek(argCount + 1);
bool gotMethod = tableGet(&klass->methods, name, &method);

if (klass->superclass != NULL) {
Value originalMethod = method;
if (gotMethod) {
// if inner is native, we do not need to bind it
if (!IS_NATIVE(*inner)) {
method = OBJ_VAL(newBoundMethod(receiver, *inner, AS_CLOSURE(method)));
}
} else {
if (!IS_NATIVE(*inner)) {
// if gotMethod is false, inner is no where in the stack, it might subject to gc
// maybe we should allow the inner to be stored in a bound method that has nativenil as the method of the bound method
method = OBJ_VAL(newBoundMethod(receiver, *inner, NULL));
}
else {
// note here copy string will not allocate new memory, as the string is already in the string table
assert(tableGet(&vm.globals, copyString(NILNATIVENAME, strlen(NILNATIVENAME)), &method));
}
}
vm.stackTop[-argCount - 1] = method;
if (invokeFromClass(klass->superclass, name, argCount, &method)) {
return true;
}
method = originalMethod;
vm.stackTop[-argCount - 1] = *inner; // as we set the stack back, bound method we created might be gc-ed
}

// superclass is null, or the method is not found in the superclass
if (!gotMethod) {
return false;
}

// inner is already on the stack
return call(AS_CLOSURE(method), argCount);
}

Optimization

One possible optimization is to create ObjBoundMethod only if the method is calling inner. We could store whether method is calling inner during compile time, and store that info inside ObjFunction. If we find a method we want to call, but it does not use inner(), then we could safely throw the inner in the stack away, and let GC to collect it.

先放上思维导图:

思维导图 (源md下载 )

备注:思维导图由markmap生成,源md文件下载后可以直接复制到Try markmap中进行修改使用

最近上完了GAMES101的课程,想简单记录一下学到的东西。作为一门图形学的入门课,这门课涉及到了关于图形学的各种各样有趣的话题(我甚至还学到了如何拍照和如何画画),质量非常高。不过我学到更多的在图形学之外。

我学到最多的是关于如何学习。当了多少年学生,我好像从来没有对如何学习这个问题产生过疑问。但通过闫老师的授课方式,我总结了两点我们学习过程中的不同:

  • why,what,how的顺序去学习:我学习的问题在于过分关注how了,比如学习算法,我会把更多注意力放在算法的细节上,而忽视了人们为什么要实现这个算法。而闫老师的授课,不是一开始就把重心放在光线追踪的算法实现,而是花了很多时间讲人们为什么要使用光线追踪,它能解决光栅化的哪些问题。我觉得这样的好处是,它使得学习变得有关联性的,比如问为什么要设计光线追踪的算法,其实就回答了光栅化和光线追踪的关系,这两个方法就不再是两个孤岛,而是串联到了一起。这样更方便知识形成体系。
  • 关注简单的东西:闫老师上课很喜欢说”简单“这个词(当然他可能确实觉得很简单>_<)。不过我觉得他很擅长把一个复杂的东西解释得简单,比如他在讲微表面模型的时候,就花了很多时间介绍微表面模型的intuition(近处看微表面是一个个微小的镜子),学完这门课,我可能无法记清微表面模型BRDF的每项如何计算,但对这个intuition的解释却印象深刻。这个intuition的解释,也可以帮助我看到微表面BRDF的公式的时候,快速理解每一项的含义。微表面模型具体的实现计算都很复杂,但背后的intuition确实是简单的,我觉得这也是我在学习的过程要去关注的。不过我也觉得intuition其实是理解事物之后高度抽象的产物(就像一个低模,虽然没有几个顶点,但看一眼就知道是什么),所以本质是要更多地理解,并锻炼抽象归纳的能力

我还学到了关于解决问题的方法: 就是要从最简单的解法开始。比如拿如何光栅化一个三角形来说,最简单的方法就是采样,但简单的采样存在问题,会走样,也没有考虑遮挡,那我们就可以分别想办法解决。我们没有办法一下就设计出一个完美的解法,但可以在发现问题,思考解决问题的循环中逐步完善一个解法。复杂的解法由简单的解法进化而来,思考简单的解法也是有意义。

另外学到的是要更多地实践。光听这个课,我可能并不会对图形学这么感兴趣。但当我可以渲染出牛牛、球球、兔兔之后,我真的觉得非常满足,它给了我即时正反馈,帮助我继续学下去。而且实践有助于我更多地思考和理解,有些问题如果不上手去做就不会发现。实践还有助于我破除恐惧,没有做的时候会觉得一个东西很复杂,但是真的做了以后感觉也还行。

还有就是就算不完美,只要看起来好看就行!我之前hw7写了一个奇奇怪怪的bug,但是生成出来的图看着挺对的,后来我发现了这个bug,尝试进行修复后,图像生成得就有问题了。结果是原来的bug完美地解决了另一个bugXD。总之我的代码可以通过两个bug成功运行,真的很不错!

好啦,感觉也写得差不多了,希望我还有下一篇博客!

0%