<?xml version="1.0" encoding="utf-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"><channel><title>AJ教程_站长资源网</title><link>https://ajaa.cn/</link><description></description><item><title>为何最终我放弃了 Go 的 sync.Pool</title><link>https://ajaa.cn/872.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172206176414892611089.jpg&quot; alt=&quot;为何最终我放弃了 Go 的 sync.Pool&quot; title=&quot;为何最终我放弃了 Go 的 sync.Pool&quot; /&gt;&lt;/p&gt;&lt;p&gt;声明: &lt;code&gt;本文并非否定 sync.Pool，而是分享技术选型的思考过程，帮助大家更准确地使用它&lt;/code&gt;&lt;br&gt; &lt;/p&gt;

&lt;p&gt;&lt;/p&gt; 
&lt;h3&gt;
一、使用场景&lt;/h3&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;一句话总结：保存和复用临时对象，减少内存分配，降低GC压力&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;h5&gt;
1.1、引入：&lt;/h5&gt; 
&lt;p&gt;举个简单的例子：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;type User struct {
    ID       int64  `json:&quot;id&quot;`
    Username string `json:&quot;username&quot;`
    Email    string `json:&quot;email&quot;`
    Profile  [512]byte `json:&quot;profile_data&quot;` // 简介
}

var buf, _ = json.Marshal(
		User{
			ID: 1, 
			Username: &quot;john_doe&quot;, 
			Email: &quot;john@example.***&quot;,
		   },
		)

 user := &amp;amp;User{}
 json.Unmarshal(buf, user)
&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;json的反序列化在&lt;code&gt;数据解析&lt;/code&gt;和&lt;code&gt;网络通信&lt;/code&gt;中非常常见，当程序&lt;code&gt;并发度&lt;/code&gt;非常高的情况下，&lt;br&gt; 短时间内需要创建&lt;code&gt;大量临时对象&lt;/code&gt;。而这些临时对象都是分配在堆上的，会给&lt;code&gt;GC造成很大的压力&lt;/code&gt;，严重影响程序的性能。&lt;br&gt; 所以可以通过&lt;code&gt;sync.Pool&lt;/code&gt;来解决。&lt;/p&gt; 
&lt;h5&gt;
1.2、什么是sync.pool？&lt;/h5&gt; 
&lt;p&gt;Go语言，从1.3版本开始提供&lt;code&gt;对象重用机制&lt;/code&gt;，即 &lt;code&gt;sync.Pool&lt;/code&gt;。&lt;br&gt; sync.Pool 是 sync 包下的一个组件，可以作为保存临时取还对象的一个“池子”。&lt;br&gt; 同时sync.Pool是&lt;code&gt;可伸缩&lt;/code&gt;且&lt;code&gt;并发安全的&lt;/code&gt;，他的大小受限于内存的大小。sync.Pool用于存储那些被分配了但是没有被使用，而未来还会使用的值。&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;这样就不用再次经过内存分配，而是直接复用对象，减轻GC压力，从而提升性能。
&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;但个人觉得它的命名可能造成误解，因为 Pool 里装的对象可以被无通知地被回收，可能 sync.Cache(临时缓存) 是一个更合适的名字。&lt;/p&gt; 
&lt;h3&gt;
二、如何使用&lt;/h3&gt; 
&lt;p&gt;sync.Pool 的使用方式非常简单：&lt;/p&gt; 
&lt;h5&gt;
2.1、声明对象池&lt;/h5&gt; 
&lt;p&gt;只需要实现New函数即可，当对象池(sync.Pool)中没有对象时，就会自动调用New函数进行。&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;var userPool = sync.Pool{
    New: func() interface{} { 
        return new(User) 
    },
}
&lt;/code&gt;&lt;/pre&gt; 
&lt;h5&gt;
2.2、GET &amp;amp; PUT&lt;/h5&gt; 
&lt;pre&gt;&lt;code&gt;// 取出
user := userPool.Get().(*User) 
json.Unmarshal(buf,user)
// 放回
userPool.Put(user) 
&lt;/code&gt;&lt;/pre&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Get()&lt;/em&gt; 用于从对象池中获取对象，因为返回值是 interface{}，因此需要类型转换。&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Put()&lt;/em&gt; 则是在对象使用完毕后，返回对象池。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h3&gt;
三、实例：&lt;/h3&gt; 
&lt;h5&gt;
3.1、标准库中的应用&lt;/h5&gt; 
&lt;h6&gt;
3.1.1: fmt.Printf&lt;/h6&gt; 
&lt;p&gt;Go语言标准库大量使用了&lt;code&gt;sync.Pool&lt;/code&gt;，例如: &lt;code&gt;fmt&lt;/code&gt;和&lt;code&gt;encoding/json&lt;/code&gt;&lt;br&gt; 以下是&lt;code&gt;fmt.Printf&lt;/code&gt;的源代码(go/src/fmt/print.go) - &lt;em&gt;你也可以到本地Go源码自行查看&lt;/em&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;// go 1.13.6

// pp is used to store a printer&#039;s state and is reused with sync.Pool to avoid allocations.
// pp用于存储打印机的状态，并与sync.Pool一起重用。以避免分配。
type pp struct {
    buf buffer
    ...
}

var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}

// newPrinter allocates a new pp struct or grabs a cached one.
// newPrinter分配了一个新的pp结构体或获取一个缓存的pp结构体。
func newPrinter() *pp {
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.wrapErrs = false
	p.fmt.init(&amp;amp;p.buf)
	return p
}

// free saves used pp structs in ppFree; avoids an allocation per invocation.
// 在ppFree中保存使用过的pp结构体；避免每次调用分配。
func (p *pp) free() {
	if cap(p.buf) &amp;gt; 64&amp;lt;&amp;lt;10 {
		return
	}

	p.buf = p.buf[:0]
	p.arg = nil
	p.value = reflect.Value{}
	p.wrappedErr = nil
	ppFree.Put(p)
}

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

// Printf formats a***ording to a format specifier and writes to standard output.
// Printf根据格式说明符进行格式化，并写入标准输出。
// It returns the number of bytes written and any write error encountered.
// 返回写入的字节数和遇到的任何写入错误。
func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}
&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;code&gt;fmt.Printf&lt;/code&gt; 的调用是非常频繁的，利用 &lt;code&gt;sync.Pool&lt;/code&gt; 复用 &lt;code&gt;pp 对象&lt;/code&gt;能够极大地提升性能，减少内存占用，同时降低 GC 压力。&lt;/p&gt; 
&lt;h5&gt;
3.2、Gin框架的应用(context)&lt;/h5&gt; 
&lt;p&gt;在Gin框架中，&lt;code&gt;Context 对象&lt;/code&gt;代表了处理一个HTTP请求的上下文。每个请求都需要一个Context，请求处理完毕，Context的生命周期也就结束了。&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;高频的创建于销毁&lt;/strong&gt;：在&lt;code&gt;高并发&lt;/code&gt;下，每秒会创建和销毁大量Context对象。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;固定生命周期&lt;/strong&gt;：Context的生命周期始于请求到来，止于请求处理完毕，非常短暂。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h6&gt;
3.2.1、定义对象池&lt;/h6&gt; 
&lt;p&gt;在&lt;code&gt;gin.Engine结构体&lt;/code&gt;的定义中，你可以看到pool字段就是一个sync.Pool&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;type Engine struct {
    // ... 其他字段
    pool sync.Pool // context 对象池
}
如下：
&lt;/code&gt;&lt;/pre&gt; 
 
&lt;h6&gt;
3.2.2、初始化对象池&lt;/h6&gt; 
&lt;p&gt;在创建Gin引擎实例的时，会初始化&lt;code&gt;sync.Pool&lt;/code&gt;，并指定New函数。&lt;br&gt; 当池子中无对象可用的时，会调用此函数创建新的Context。&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;func New() *Engine {
    // ...
    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    return engine
}

func (engine *Engine) allocateContext(maxParams uint16) *Context {
    // 分配并初始化一个Context
    v := make(Params, 0, maxParams)
    return &amp;amp;Context{engine: engine, params: &amp;amp;v, skippedNodes: &amp;amp;skippedNodes}
}
&lt;/code&gt;&lt;/pre&gt; 
&lt;h6&gt;
3.2.3、从池中获取Context&lt;/h6&gt; 
&lt;p&gt;当HTTP请求到达时，Gin会从&lt;code&gt;sync.Pool&lt;/code&gt;中获取一个&lt;code&gt;Context对象&lt;/code&gt;。&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 从对象池中获取一个 context[citation:7]
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()
    // ... 处理 http 请求
    engine.handleHTTPRequest(c)
    // 把 context 放回对象池[citation:7]
    engine.pool.Put(c)
}
&lt;/code&gt;&lt;/pre&gt; 
&lt;h6&gt;
3.2.4、处理请求后放回池中&lt;/h6&gt; 
&lt;p&gt;请求处理完毕后，Gin会将&lt;code&gt;Contex&lt;/code&gt;重置并放回&lt;code&gt;sync.Pool&lt;/code&gt;中，以供后面复用。&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    // ... 处理 http 请求
    engine.handleHTTPRequest(c)
    // 请求处理完成后，将 Context 放回池中[citation:7]
    engine.pool.Put(c)
}
&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;切记，重点是要重置的，如调用c.reset()。确保放回的是&lt;code&gt;干净&lt;/code&gt;的上下文。&lt;/p&gt; 
&lt;h3&gt;
四、我在项目中的实战&lt;/h3&gt; 
&lt;h5&gt;
4.1、为何最初选择sync.Pool&lt;/h5&gt; 
&lt;p&gt;因以后其他博客还会提及，所以这里就简洁的说一下：&lt;br&gt; &lt;strong&gt;我的目的：&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;设计了一个支持多存储驱动的图片上传模块，重点解决了并发性能、资源管理和动态切换的问题
&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;为了解决所谓的高并发，复用实例的问题，我就自然的想到去使用sync.Pool，但问题来了！&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;对象复用&lt;/strong&gt;：避免频繁创建和销毁对象&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;并发安全&lt;/strong&gt;：多个用户可同时使用不同驱动&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;为此，我还美滋滋的，描绘了一个草图：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;// 多驱动对象池管理器
type MultiDriverPool struct {
    pools   map[string]ObjectPool
    mu      sync.RWMutex
    current string // 当前默认驱动
}
// 对象池接口
type ObjectPool interface {
    Get() (Driver, error)
    Put(Driver) error
    Close()
    Size() int
    Available() int
}
&lt;/code&gt;&lt;/pre&gt; 
 
&lt;h5&gt;
4.2、又为何选择放弃sync.pool&lt;/h5&gt; 
&lt;h6&gt;
4.2.1、存储驱动通常是无状态的&lt;/h6&gt; 
&lt;p&gt;&lt;strong&gt;比如：&lt;/strong&gt; 七牛云驱动使用相同的A***essKey和SecretKey，&lt;code&gt;每个实例&lt;/code&gt;都&lt;code&gt;执行相同的操作&lt;/code&gt;，没有必要维护多个实例。实际上，&lt;code&gt;一个驱动实例就可以处理所有请求&lt;/code&gt;，而且通常驱动本身是&lt;code&gt;线程安全&lt;/code&gt;的（或者可以通过在方法内部分配资源来做到线程安全）&lt;/p&gt; 
&lt;p&gt;**换句话说就是：**认为每个驱动实例需要频繁创建和销毁，但实际上驱动实例是可以复用的，而且创建成本不高，并且“存储驱动是无状态的” ！&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;所以我最终的设计模式是：单例+多驱动模式。
&lt;/code&gt;&lt;/pre&gt; 
&lt;h3&gt;
五、总结&lt;/h3&gt; 
&lt;p&gt;适合 sync.Pool 的场景：&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;创建成本高&lt;/strong&gt; 对象初始化有显著开销&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;生命周期短&lt;/strong&gt; 使用后很快就不再需要&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;使用频率高&lt;/strong&gt; 大量并发创建销毁&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;可安全重置&lt;/strong&gt; 能完全清理之前的状态&lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;不适合：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;// 1、存储驱动 - 创建成本低，生命周期长
var driverPool = sync.Pool{
    New: func() interface{} { return &amp;amp;QiniuDriver{} },
}

// 2、 数据库连接 - 需要连接池，不是对象池
var dbPool = sync.Pool{
    New: func() interface{} { return sql.Open(...) },
}

// 3、配置对象 - 长期存在，不需要频繁创建
var configPool = sync.Pool{
    New: func() interface{} { return loadConfig() },
}
&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;在结尾处，我在声明一下：&lt;br&gt; sync.Pool 的核心作用，不是资源管理。&lt;br&gt; 而是通过&lt;strong&gt;保存和复用临时对象，减少内存分配，降低GC压力！&lt;/strong&gt;&lt;/p&gt; 
&lt;h3&gt;
六、sync.Pool的底层剖析&lt;/h3&gt; 
&lt;h4&gt;
6.1 底层结构体&lt;/h4&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-go&quot;&gt;&lt;span class=&quot;token ***ment&quot;&gt;// [Go 内存模型]: https://go.dev/ref/mem&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; Pool &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	noCopy noCopy

	local     unsafe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Pointer &lt;span class=&quot;token ***ment&quot;&gt;// 每个 P 的本地固定大小池，实际类型是 [P]poolLocal&lt;/span&gt;
	localSize &lt;span class=&quot;token builtin&quot;&gt;uintptr&lt;/span&gt;        &lt;span class=&quot;token ***ment&quot;&gt;// 本地数组的大小&lt;/span&gt;

	victim     unsafe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Pointer &lt;span class=&quot;token ***ment&quot;&gt;// 上一个周期的本地池&lt;/span&gt;
	victimSize &lt;span class=&quot;token builtin&quot;&gt;uintptr&lt;/span&gt;        &lt;span class=&quot;token ***ment&quot;&gt;// victim 数组的大小&lt;/span&gt;

	&lt;span class=&quot;token ***ment&quot;&gt;// New 可以选择性地指定一个函数，用于在 Get 否则会返回 nil 时生成一个值。&lt;/span&gt;
	&lt;span class=&quot;token ***ment&quot;&gt;// 不能在与 Get 调用并发的情况下修改此函数。&lt;/span&gt;
	New &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; any
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; 
&lt;h4&gt;
6.2 重点&lt;/h4&gt; 
&lt;p&gt;在Pool的底层，核心有两点：分别是local与victim&lt;/p&gt; 
&lt;h5&gt;
6.2.1 local unsafe.Pointer&lt;/h5&gt; 
&lt;p&gt;local 是一个 按 P（GOMAXPROCS）分片的本地对象池。&lt;br&gt; 每个 P 都有自己的 poolLocal，无需锁，极快。&lt;br&gt; Get / Put 操作优先访问本地池，不需要加锁。&lt;/p&gt; 
&lt;h5&gt;
6.2.2 victim&lt;/h5&gt; 
&lt;p&gt;Go 认为 Pool 内的对象是&lt;strong&gt;可丢弃的&lt;/strong&gt;，所以 每次 GC 会清空 pool.local。&lt;br&gt; 为了避免冲击（比如刚清空就马上又需要大量对象），Go 引入了：&lt;strong&gt;上一 GC 周期的 pool.local 备份&lt;/strong&gt;。&lt;br&gt; 避免 GC 后对象全部被清空导致性能抖动。&lt;br&gt; &lt;strong&gt;victim的大致流程如下：&lt;/strong&gt;&lt;br&gt; &lt;br&gt; &lt;/p&gt; 
&lt;h3&gt;
七、性能测试&lt;/h3&gt; 
&lt;h4&gt;
7.1 测试主函数&lt;/h4&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-go&quot;&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; User &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	ID       &lt;span class=&quot;token builtin&quot;&gt;int64&lt;/span&gt;     &lt;span class=&quot;token string&quot;&gt;`json:&quot;id&quot;`&lt;/span&gt;
	Username &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;    &lt;span class=&quot;token string&quot;&gt;`json:&quot;username&quot;`&lt;/span&gt;
	Email    &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;    &lt;span class=&quot;token string&quot;&gt;`json:&quot;email&quot;`&lt;/span&gt;
	Profile  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;512&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;byte&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;`json:&quot;profile_data&quot;`&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 创建 User Pool&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; userPool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; sync&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Pool&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;New&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	atomic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;AddUint64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;poolMisses&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 创建 Buffer Pool&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; bufPool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; sync&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Pool&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;New&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bytes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 调用Get的次数&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; totalGets &lt;span class=&quot;token builtin&quot;&gt;uint64&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 必须创建新对象的次数&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; poolMisses &lt;span class=&quot;token builtin&quot;&gt;uint64&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 获取一个 User&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;User &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	atomic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;AddUint64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;totalGets&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; userPool&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 放回 User&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;putUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;u &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token ***ment&quot;&gt;// 1、清空数据&lt;/span&gt;
	u&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ID &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
	u&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Username &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
	u&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Email &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; u&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Profile &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		u&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Profile&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token ***ment&quot;&gt;// 2、放回&lt;/span&gt;
	userPool&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 处理 User&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;processUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;User &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	u &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; json&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Unmarshal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; u
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// 处理 HTTP 请求&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleProcess&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;w http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ResponseWriter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token ***ment&quot;&gt;// 1、获取 Buffer&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; b bytes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Buffer

	&lt;span class=&quot;token ***ment&quot;&gt;// 2、获取 User&lt;/span&gt;
	&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; io&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Copy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Body&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

	&lt;span class=&quot;token ***ment&quot;&gt;// 3、处理 User&lt;/span&gt;
	u &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;processUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Bytes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;putUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token ***ment&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleMetrics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;w http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ResponseWriter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	hits &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; atomic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;LoadUint64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;totalGets&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; atomic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;LoadUint64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;poolMisses&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; w&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;sync_pool_gets &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; strconv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;FormatUint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;atomic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;LoadUint64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;totalGets&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;\n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; w&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;sync_pool_misses &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; strconv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;FormatUint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;atomic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;LoadUint64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;poolMisses&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;\n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; w&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;sync_pool_hits &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; strconv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;FormatUint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hits&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;\n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;HandleFunc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/process&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handleProcess&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;HandleFunc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/metrics&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handleMetrics&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ListenAndServe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;:8080&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt; 
&lt;h4&gt;
7.2 对象的复用率&lt;/h4&gt; 
&lt;pre&gt;&lt;code&gt;
// TestHTTPConcurrent
// 测试结果：totalGets=500, poolMisses=28
func TestHTTPConcurrent(t *testing.T) {
	data := []byte(`{&quot;id&quot;:10,&quot;username&quot;:&quot;concurrent&quot;,&quot;email&quot;:&quot;c@example.***&quot;}`)
	req := httptest.NewRequest(http.MethodPost, &quot;/process&quot;, bytes.NewBuffer(data))

	n := 500
	var wg sync.WaitGroup
	wg.Add(n)
	for i := 0; i &amp;lt; n; i++ {
		go func() {
			defer wg.Done()
			w := httptest.NewRecorder()
			handleProcess(w, req)
			if w.Code != http.StatusOK {
				t.Errorf(&quot;bad status&quot;)
			}
		}()
	}
	wg.Wait()

	t.Logf(&quot;totalGets=%d, poolMisses=%d&quot;, totalGets, poolMisses)
}

&lt;/code&gt;&lt;/pre&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;=== RUN TestHTTPConcurrent&lt;br&gt; pool_test.go:65: &lt;strong&gt;totalGets=500&lt;/strong&gt;（调用get的总次数）,&lt;strong&gt;poolMisses=4&lt;/strong&gt;（新new的次数）&lt;br&gt; — PASS: TestHTTPConcurrent (0.00s)&lt;br&gt; PASS&lt;br&gt; 但若大家自己测，由于处于不同环境，结果应该会有些许波动。&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;h4&gt;
7.3 对象复用性能测试&lt;/h4&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-go&quot;&gt;采用 基准测试 ：用来测性能的测试，包括耗时、内存分配、GC 压力等

&lt;span class=&quot;token ***ment&quot;&gt;// b.N 是测试循环次数（Go 自动调整）&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;// b.ReportAllocs()：显示内存分配次数&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;// b.ResetTimer()：重置计时器（忽略前面初始化的耗时）&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// ------------------------&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;// Benchmark - 无对象池&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;// ------------------------&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;BenchmarkWithoutPool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;testing&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;B&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	data &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;`{&quot;id&quot;:123,&quot;username&quot;:&quot;user123&quot;,&quot;email&quot;:&quot;user123@example.***&quot;}`&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ReportAllocs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ResetTimer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;N&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		u &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; json&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Unmarshal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;// ------------------------&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;// Benchmark - 对象池&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;// ------------------------&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;BenchmarkWithPool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;testing&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;B&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	data &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;`{&quot;id&quot;:123,&quot;username&quot;:&quot;user123&quot;,&quot;email&quot;:&quot;user123@example.***&quot;}`&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ReportAllocs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ResetTimer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;N&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		u &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; json&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Unmarshal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;putUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;测试项&lt;/th&gt;
&lt;th&gt;ns/op（每次操作耗时）&lt;/th&gt;
&lt;th&gt;B/op（分配内存字节数）&lt;/th&gt;
&lt;th&gt;allocs/op（分配次数）&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WithoutPool&lt;/td&gt;
&lt;td&gt;721 ns&lt;/td&gt;
&lt;td&gt;816 B&lt;/td&gt;
&lt;td&gt;7 allocs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WithPool&lt;/td&gt;
&lt;td&gt;664 ns&lt;/td&gt;
&lt;td&gt;240 B&lt;/td&gt;
&lt;td&gt;6 allocs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以从，&lt;strong&gt;B/op（分配内存字节数）&lt;/strong&gt;，近4倍的差距，看出性能的差距。&lt;br&gt; 当然大家自测时，应该会出现偏差，要以withoutPool与withPool的差距作为对比标准。&lt;/p&gt; 
&lt;h3&gt;
八、自测&lt;/h3&gt; 
&lt;ol&gt;
&lt;li&gt;sync.Pool 的主要作用是什么？为什么它能减少 GC 压力？&lt;/li&gt;
&lt;li&gt;sync.Pool.New 是在什么情况下被调用的？&lt;/li&gt;
&lt;li&gt;为什么从 pool 取出的对象必须 重置（reset）？&lt;/li&gt;
&lt;li&gt;为什么在你的代码中，putUser() 必须把结构体所有字段清空？(原：str = “” 设为空)&lt;/li&gt;
&lt;li&gt;为什么 结构体字段清空了却依旧要 Reset()？&lt;/li&gt;
&lt;li&gt;sync.Pool 为什么不是普通的缓存？它有什么生命周期特性？&lt;/li&gt;
&lt;/ol&gt; 
&lt;hr&gt; 
&lt;p&gt;借鉴：&lt;br&gt; 1、Go 语言高性能编程 - sync.pool&lt;br&gt; 2、 深度解密 Go 语言之 sync.Pool &lt;/p&gt; 
&lt;hr&gt;</description><pubDate>Wed, 26 Nov 2025 17:22:04 +0800</pubDate></item><item><title>【MCP探索实践】Google GenAI Toolbox：Google开源的企业级AI数据库中间件、5分钟搞定LLM-SQL安全互联</title><link>https://ajaa.cn/871.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172204176414892414145.jpg&quot; alt=&quot;【MCP探索实践】Google GenAI Toolbox：Google开源的企业级AI数据库中间件、5分钟搞定LLM-SQL安全互联&quot; title=&quot;【MCP探索实践】Google GenAI Toolbox：Google开源的企业级AI数据库中间件、5分钟搞定LLM-SQL安全互联&quot; /&gt;&lt;/p&gt;&lt;h2&gt;
系列篇章💥&lt;/h2&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;No.&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;文章&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;1&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Cherry Studio+MCP实战：3步让AI自动抓网页/读文件/调API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】FastAPI + MCP：2025年最火的后端与AI集成方案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;3&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】GitHub MCP Server：为开发者打造的高效自动化工具&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;4&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】MoLing：零依赖跨平台办公自动化神器，3分钟搞定文件+浏览器双核操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;5&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】3分钟搭建AI服务器！FastMCP让开发效率飙升10倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;6&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】MindsDB：借助 MCP 协议，让 AI 大模型秒变 SQL 专家&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;7&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Web Search MCP Server：无需 API 密钥的免费网络搜索服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;8&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】百度地图 MCP Server：告别繁琐集成、让地图服务接入更简单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;9&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】MCP生态下的LangChain适配器：AI开发的“加速引擎”，多工具集成一步到位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;10&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】OpenMemory MCP：如何用MCP协议解锁AI工具的跨平台记忆共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;11&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Playwright MCP：微软打造的AI自动化利器，一键搞定浏览器操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;12&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】ROS MCP Server：自然语言控制机器人，从此告别复杂指令！&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;13&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】蚂蚁AntV开源的可视化图表MCP Server Chart：高效数据可视化的利器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;14&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Firecrawl MCP Server：为LLM客户端赋能的开源Web爬虫服务器，数据采集效率提升10倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;15&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】MiniMax MCP Server：多模态生成服务器，让AI同时玩转视频/语音/图像生成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;16&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Bright Data MCP：实时、安全、智能，网络数据抓取的三剑客来袭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;17&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Chrome MCP Server：基于Chrome扩展的AI浏览器自动化神器——技术解析与实践指南&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;18&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Redis官方MCP Server：用自然语言驱动Redis的AI原生存储引擎&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;19&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】MCP MongoDB Server：让LLM与MongoDB无缝交互&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;20&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Windows-MCP：开源 AI Agent 一键打通 Windows 全接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;21&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】mcp-installer：一键部署MCP服务器的高效工具&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;22&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】MCP-Shield：守护MCP服务器安全的利器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;23&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;【MCP探索实践】Google GenAI Toolbox：Google开源的企业级AI数据库中间件、5分钟搞定LLM-SQL安全互联&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt; 
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt; 
&lt;hr&gt; 
&lt;h2&gt;
前言&lt;/h2&gt; 
&lt;p&gt;随着生成式 AI 进入生产环境，开发者急需一种“低代码、高安全、可观测”的方式来把 LLM 与关系型数据库打通。Google 2024 年开源的 genai-toolbox（MCP Toolbox for Databases）正是为此而生，它通过统一的服务端代理 + 多语言 SDK，把 SQL 查询包装成 LLM 可调用的工具，10 行代码即可上线，极大降低 RAG、智能报表、Agent 等场景的开发门槛。&lt;br&gt; &lt;/p&gt; 
&lt;h2&gt;
一、项目概述&lt;/h2&gt; 
&lt;p&gt;genai-toolbox 是一款面向企业级场景的 MCP（Model-Context-Protocol）服务器开源工具箱，用 Go 语言实现，内置连接池、身份鉴权、OpenTelemetry 链路追踪，&lt;code&gt;可把 PostgreSQL、AlloyDB 等数据库表/视图快速映射成 LLM 可调用的 Function Calling 工具&lt;/code&gt;，并支持 Python、Node.js、Go、Java 等多语言 SDK 集成。&lt;br&gt; &lt;/p&gt; 
&lt;h2&gt;
二、技术原理&lt;/h2&gt; 
&lt;h3&gt;
（一）、整体架构&lt;/h3&gt; 
&lt;blockquote&gt; 
 &lt;ol&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;Server 端&lt;/strong&gt;： 解析 YAML 配置，建立数据库连接池； 暴露 RESTful API：/loadToolset、/invokeTool 等； 内嵌 auth 中间件（OAuth2/JWT）与 OpenTelemetry Collector，实现零侵入可观测。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;Client SDK&lt;/strong&gt;： 封装 HTTP 调用，提供异步 loadToolset()； 将工具元数据（name、description、JSONSchema）转成 LangChain、LlamaIndex、Genkit 等框架的 Tool 对象。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;MCP 协议&lt;/strong&gt;： 通过统一的 JSON-RPC 风格协议，让 LLM 在对话中以 Function Calling 方式调用 SQL，Server 端负责参数校验、SQL 预编译、结果序列化。&lt;/p&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;/blockquote&gt; 
&lt;h3&gt;
（二）、性能与安全&lt;/h3&gt; 
&lt;blockquote&gt; 
 &lt;ol&gt;
&lt;li&gt;连接池复用 + Prepared Statement 防注入；&lt;/li&gt;
&lt;li&gt;支持 IAM 集成、SSL/TLS 加密、行级权限控制；&lt;/li&gt;
&lt;li&gt;提供开箱即用的 Prometheus Metrics（latency、qps、error rate）。&lt;/li&gt;
&lt;/ol&gt; 
&lt;/blockquote&gt; 
&lt;h2&gt;
三、主要功能&lt;/h2&gt; 
&lt;h3&gt;
（一）、核心能力&lt;/h3&gt; 
&lt;blockquote&gt; 
 &lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;零代码 SQL 转换工具&lt;/strong&gt;：&lt;code&gt;只需在 tools.yaml 文件中声明 SQL 语句及其参数，系统便能自动生成相应工具&lt;/code&gt;，显著降低了开发成本和技术门槛，让开发者无需编写大量代码即可实现功能。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;多数据源支持&lt;/strong&gt;：&lt;code&gt;全面支持多种数据库，包括 PostgreSQL、AlloyDB、Cloud SQL 以及处于实验阶段的 MySQL&lt;/code&gt;，能够满足不同企业多样化的数据库使用需求，为企业的数据管理提供了更多的选择和灵活性。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;工具集高效管理&lt;/strong&gt;：工具集按照业务模块进行分组，支持版本化发布与灰度更新。这使得在工具的更新和维护过程中，可以更加精准地控制范围，降低风险，确保系统的稳定性和可靠性。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;全生态 SDK 覆盖&lt;/strong&gt;：提供了丰富的 SDK 支持，涵盖 Python、Node.js、Go、Java 等多种编程语言，以及 LangChain、LlamaIndex、Genkit 等主流框架。无论开发者使用何种技术栈，都能方便地集成该工具，实现与系统的无缝对接。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;端到端观测能力&lt;/strong&gt;：具备完整的端到端观测体系，集成了 OpenTelemetry Trace、Prometheus Metrics 以及结构化日志。通过这些观测手段，开发者可以实时监控系统的运行状态，及时发现并解决潜在问题，保障系统的稳定运行。&lt;/li&gt;
&lt;/ol&gt; 
&lt;/blockquote&gt; 
&lt;h3&gt;
（二）、高级特性&lt;/h3&gt; 
&lt;blockquote&gt; 
 &lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;向量 SQL 功能&lt;/strong&gt;：&lt;code&gt;内置 text_embedding() 调用，支持向量相似度检索（借助 pgvector 扩展）。&lt;/code&gt;这一特性使得系统能够处理复杂的语义搜索和分析任务，为企业提供更智能、高效的数据分析服务。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;事务级工具支持&lt;/strong&gt;：&lt;code&gt;在一次对话内，多个工具可以共享同一连接事务。&lt;/code&gt;这种设计确保了数据操作的一致性和完整性，避免了因事务处理不当而导致的数据错误，提高了系统的可靠性和数据安全性。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;动态参数校验机制&lt;/strong&gt;：&lt;code&gt;采用 JSONSchema 与 SQL 类型映射的方式，实现动态参数校验。&lt;/code&gt;在运行时，系统能够自动检查参数的合法性，减少因参数错误而导致的运行时错误，提高了系统的稳定性和健壮性。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;热加载功能&lt;/strong&gt;：当配置发生修改时，无需重启系统，配置修改可在秒级内生效。这一特性大大提高了系统的灵活性和可维护性，使得开发者能够快速响应业务需求的变化，及时调整系统配置。&lt;/li&gt;
&lt;/ol&gt; 
&lt;/blockquote&gt; 
&lt;h2&gt;
四、应用场景&lt;/h2&gt; 
&lt;h3&gt;
（一）、RAG 知识库&lt;/h3&gt; 
&lt;p&gt;在企业知识管理领域，可将企业知识库表精准映射为 search_docs 工具。当用户提出问题时，大语言模型（LLM）能够依据问题实时召回排名前 K 的相关段落，为用户提供准确且高效的知识检索服务，极大地提升了企业内部知识的利用效率。&lt;/p&gt; 
&lt;h3&gt;
（二）、NL2SQL 数据助手&lt;/h3&gt; 
&lt;p&gt;对于运营人员和分析师而言，无需再花费大量时间学习和编写复杂的 SQL 语句。他们可以直接使用自然语言来查询订单信息、库存状况以及用户行为数据等。通过该工具，自然语言能够被准确转化为 SQL 查询，为数据分析和决策提供了极大的便利。&lt;/p&gt; 
&lt;h3&gt;
（三）、智能客服 Agent&lt;/h3&gt; 
&lt;p&gt;智能客服场景中，结合订单表、物流表以及知识库表等多源数据，智能客服 Agent 能够实现诸如“查订单→改地址→退差价”等多步决策流程。在与客户的交互过程中，它可以根据客户需求，灵活调用不同的数据表，提供一站式的优质服务。&lt;/p&gt; 
&lt;h3&gt;
（四）、低代码 BI&lt;/h3&gt; 
&lt;p&gt;在商业智能领域，前端用户可以通过简单的拖拽操作来生成查询条件。而后端则会调用 Toolbox 工具，将查询结果以 JSON 格式返回，直接用于图表的渲染。这种低代码的方式大大降低了 BI 开发的门槛，使得业务人员也能够轻松实现数据可视化。&lt;/p&gt; 
&lt;h3&gt;
（五）、AIOps&lt;/h3&gt; 
&lt;p&gt;在运维管理方面，SRE（站点可靠性工程师）只需在 Slack Bot 中输入如“最近 10 分钟错误率最高的服务”这样的自然语言指令，Toolbox 就能实时查询 Prometheus 落地表，并迅速返回查询结论，帮助运维人员及时发现和解决系统问题，保障系统的稳定运行。&lt;/p&gt; 
&lt;h2&gt;
五、快速使用&lt;/h2&gt; 
&lt;h3&gt;
（一）、环境准备&lt;/h3&gt; 
&lt;ol&gt;
&lt;li&gt;OS：Linux/macOS/Windows WSL2；&lt;/li&gt;
&lt;li&gt;Docker ≥ 20.10 或直接下载二进制；&lt;/li&gt;
&lt;li&gt;PostgreSQL 12+（或 AlloyDB）已运行，示例数据库 toolbox_db 已创建。&lt;/li&gt;
&lt;/ol&gt; 
&lt;h3&gt;
（二）、5 分钟上手&lt;/h3&gt; 
&lt;ol&gt;&lt;li&gt;下载二进制&lt;/li&gt;&lt;/ol&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;VERSION&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.2&lt;/span&gt;.0
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-O&lt;/span&gt; https://storage.googleapis.***/genai-toolbox/v&lt;span class=&quot;token variable&quot;&gt;${VERSION}&lt;/span&gt;/linux/amd64/toolbox
&lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; +x toolbox
&lt;/code&gt;&lt;/pre&gt; 
&lt;ol start=&quot;2&quot;&gt;&lt;li&gt;创建 tools.yaml&lt;/li&gt;&lt;/ol&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;sources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;my-pg&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres
    &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 127.0.0.1
    &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5432&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; toolbox_db
    &lt;span class=&quot;token key atrule&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres
    &lt;span class=&quot;token key atrule&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;search_user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;sql
    &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pg
    &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 根据姓名模糊查询用户
    &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; name
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
    &lt;span class=&quot;token key atrule&quot;&gt;statement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; SELECT id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; email FROM users WHERE name ILIKE &#039;%&#039; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; $1 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &#039;%&#039;;
&lt;/code&gt;&lt;/pre&gt; 
&lt;ol start=&quot;3&quot;&gt;&lt;li&gt;启动服务&lt;/li&gt;&lt;/ol&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;./toolbox &lt;span class=&quot;token parameter variable&quot;&gt;--tools_file&lt;/span&gt; tools.yaml &lt;span class=&quot;token parameter variable&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; 
&lt;ol start=&quot;4&quot;&gt;&lt;li&gt;Python 客户端调用&lt;/li&gt;&lt;/ol&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;pip &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; toolbox-core
from toolbox_core &lt;span class=&quot;token function&quot;&gt;import&lt;/span&gt; ToolboxClient
&lt;span class=&quot;token function&quot;&gt;import&lt;/span&gt; asyncio

async def main&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;:
    async with ToolboxClient&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;http://127.0.0.1:5000&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; as client:
        tools &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; await client.load_toolset&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;default&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; await tools&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;search_user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;.invoke&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;alice&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        print&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

asyncio.run&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; 
&lt;ol start=&quot;5&quot;&gt;&lt;li&gt;LangChain 集成（可选）&lt;/li&gt;&lt;/ol&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;pip &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; toolbox-langchain
from toolbox_langchain &lt;span class=&quot;token function&quot;&gt;import&lt;/span&gt; ToolboxClient
client &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ToolboxClient&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;http://127.0.0.1:5000&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
tools &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; client.load_toolset&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
agent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; initialize_agent&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tools, llm, &lt;span class=&quot;token assign-left variable&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;zero-shot-react-description&quot;&lt;/span&gt;, &lt;span class=&quot;token assign-left variable&quot;&gt;verbose&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;True&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
agent.run&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;帮我找出所有名字包含 alice 的用户&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; 
&lt;h3&gt;
（三）、Docker 一键部署&lt;/h3&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; toolbox &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;:5000 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/tools.yaml:/tools.yaml &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  ghcr.io/googleapis/genai-toolbox:v0.2.0 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--tools_file&lt;/span&gt; /tools.yaml
&lt;/code&gt;&lt;/pre&gt; 
&lt;h3&gt;
（四）、Kuber***es 生产级部署示例&lt;/h3&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; toolbox
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; toolbox &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; toolbox &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; toolbox
        &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/googleapis/genai&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;toolbox&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;v0.2.0
        &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--tools_file=/config/tools.yaml&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; config
          &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /config
      &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; config
        &lt;span class=&quot;token key atrule&quot;&gt;configMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; toolbox&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;config
&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;配合 HorizontalPodAutoscaler 可根据 QPS 自动扩缩容。&lt;/p&gt; 
&lt;h3&gt;
（五）、常见踩坑与排查&lt;/h3&gt; 
&lt;ol&gt;
&lt;li&gt;连接拒绝：确认 PostgreSQL 监听 0.0.0.0 且防火墙放行 5432；&lt;/li&gt;
&lt;li&gt;工具未找到：检查 toolset 名称是否匹配，或执行 &lt;code&gt;./toolbox validate --tools_file tools.yaml&lt;/code&gt; 做预检；&lt;/li&gt;
&lt;li&gt;高并发超时：在 YAML 中调大 &lt;code&gt;max_connections&lt;/code&gt; 并开启连接池 &lt;code&gt;pool_size: 20&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt; 
&lt;h2&gt;
结语&lt;/h2&gt; 
&lt;p&gt;genai-toolbox 把“让 LLM 安全、高效地访问数据库”这一复杂命题封装成一条 YAML + 十行代码，使开发者得以专注业务逻辑而非底层连接、鉴权与可观测。随着 Google 社区持续迭代（路线图已规划支持 BigQuery、Spanner、Cloud SQL Auth Proxy），它有望成为 GenAI 时代数据库中间件的事实标准。现在就动手试试吧！&lt;/p&gt; 
&lt;h2&gt;
项目地址&lt;/h2&gt; 
&lt;p&gt;GitHub 源码：https://github.***/googleapis/genai-toolbox&lt;br&gt; 官方文档：https://cloud.google.***/alloydb/docs/genai-toolbox&lt;br&gt; Codelabs 实战：https://codelabs.developers.google.***/genai-toolbox-for-alloydb&lt;/p&gt; 
&lt;hr&gt; 
 
&lt;p&gt;🎯🔖更多专栏系列文章：&lt;strong&gt;AI大模型提示工程完全指南&lt;/strong&gt;、&lt;strong&gt;AI大模型探索之路（零基础入门）&lt;/strong&gt;、&lt;strong&gt;AI大模型预训练微调进阶&lt;/strong&gt;、&lt;strong&gt;AI大模型开源精选实践&lt;/strong&gt;、&lt;strong&gt;AI大模型RAG应用探索实践&lt;/strong&gt;🔥🔥🔥 其他专栏可以查看&lt;strong&gt;博客主页&lt;/strong&gt;📑&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;😎 &lt;strong&gt;作者介绍&lt;/strong&gt;：资深程序老猿，从业10年+、互联网系统架构师，目前专注于AIGC的探索（CSDN博客之星|AIGC领域优质创作者）&lt;br&gt; 📖&lt;strong&gt;专属社群&lt;/strong&gt;：欢迎关注【&lt;strong&gt;小兵的AI视界&lt;/strong&gt;】公众号或扫描下方👇二维码，回复‘&lt;strong&gt;入群&lt;/strong&gt;’ 即刻上车，获取邀请链接。&lt;br&gt; 💘&lt;font color=&quot;purple&quot;&gt;&lt;strong&gt;领取三大专属福利&lt;/strong&gt;&lt;/font&gt;：1️⃣免费赠送AI+编程📚&lt;strong&gt;500本&lt;/strong&gt;，2️⃣AI技术教程副业资料&lt;strong&gt;1套&lt;/strong&gt;，3️⃣DeepSeek资料教程&lt;strong&gt;1套&lt;/strong&gt;🔥（限前500人）&lt;br&gt; &lt;strong&gt;如果文章内容对您有所触动，别忘了&lt;font color=&quot;red&quot;&gt;&lt;strong&gt;点赞、⭐关注，收藏&lt;/strong&gt;&lt;/font&gt;！加入我们，一起携手同行AI的探索之旅，开启智能时代的大门！&lt;/strong&gt;&lt;/p&gt; 
&lt;/blockquote&gt;</description><pubDate>Wed, 26 Nov 2025 17:22:02 +0800</pubDate></item><item><title>π0源码(openpi)剖析——从π0模型架构的实现：如何基于PaLI-Gemma和扩散策略去噪生成动作，到基于C/S架构下的模型训练与部署</title><link>https://ajaa.cn/870.html</link><description>&lt;p&gt;&lt;img src=&quot;https://img2.baidu.com/it/u=3692272688,1591369627&amp;fm=253&amp;fmt=auto&amp;app=120&amp;f=JPEG?w=931&amp;h=500&quot; /&gt;&lt;/p&gt;&lt;h2 id=&quot;%E5%89%8D%E8%A8%80%C2%A0&quot; name=&quot;%E5%89%8D%E8%A8%80%C2%A0&quot;&gt;前言&lt;/h2&gt; 
&lt;p&gt;ChatGPT出来后的两年多，也是我疯狂写博的两年多(年初deepseek更引爆了下)，比如从创业起步时的15年到后来22年之间 每年2-6篇的，干到了23年30篇、24年65篇、25年前两月18篇，成了我在大模型和具身的原始技术积累&lt;/p&gt; 
&lt;p&gt;如今一转眼已到25年3月初，时光走得太快，近期和团队接了好几个大客户订单，使得3月起 不得不全力加速落地，自己也得每天抠paper、搞代码&lt;/p&gt; 
&lt;p&gt;so，为何在明明如此之忙 一天当两天用的情况下，还要继续努力更新博客呢？&lt;/p&gt; 
&lt;p&gt;原因在于&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;一方面，我&lt;strong&gt;确实喜欢分享，因为写博的这10多年下来 确实可以帮到很多、很多人&lt;/strong&gt;，不然本博客也不会有如今如此巨大的访问量与影响力&lt;br&gt; 更何况有些文章是之前既定计划中的，在本文之前，上一篇关于π0的文章是π0_fast《π0开源了且推出自回归版π0-FAST——打造机器人动作专用的高效Tokenizer：比扩散π0的训练速度快5倍但效果相当》，文中提到，会解读π0的源码&lt;br&gt; &lt;br&gt; &lt;em&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;至于&lt;/span&gt;&lt;strong&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;什么是π0&lt;/span&gt;&lt;/strong&gt;&lt;/em&gt;  &lt;em&gt;&lt;strong&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;详见此文《π0——用于通用机器人控制的VLA模型：一套框架控制7种机械臂(基于PaliGemma和流匹配的3B模型)》&lt;/span&gt;&lt;/strong&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;二方面，我司「七月在线」在做一系列工厂落地场景的过程中，我们也希望团结到可以和我们一块做的朋友，而若想团结，便需要借助博客 顺带分享我们每个季度在重点做的业务场景&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;比如过去一周，我把lerobot、reflect vlm、π0的仿真环境都在我自己本地电脑上跑了下&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;过程中，GitHub copilot这种AI编程工具在环境的安装上帮了我很大的忙——各种环境 只要几句命令，直接帮我装好，真心不错&lt;/em&gt;&lt;/span&gt;)&lt;/p&gt; 
  
 &lt;p&gt;如此硬着头皮冥思苦想、摸索了好几天，随后使得我自己知道怎么带队完成『太多工厂希望实现的一个生产线任务』了，3月初先仿真训练，2-3个月内部署到真机&lt;/p&gt; 
 &lt;p&gt;当然了，也不单纯只是「这几天的想」就能想出来的，​这几天之前&lt;/p&gt; 
 &lt;ol&gt;
&lt;li&gt;有把过去一年当三年用的具身技术积累&lt;/li&gt;
&lt;li&gt;有一年多来，和同事们 如姚博士，以及朋友们许多的讨论&lt;/li&gt;
&lt;li&gt;有去年十几个工厂对我们的支持与信任&lt;/li&gt;
&lt;/ol&gt; 
 &lt;p&gt;我们正在不断壮大队伍&lt;/p&gt; 
 &lt;ul&gt;
&lt;li&gt;有我司内部同事，亦有我联合带的北理、中南等985的具身研究生，及一块合作开发的朋友，很快会把多个生产线任务并行开发起来&lt;/li&gt;
&lt;li&gt;且无论哪个项目，都是不断长期迭代的，故过程中少不了科研层面的突破，欢迎更多伙伴加入我们(全职、兼职、实习皆可，有意者，敬请私我)，和我们一块开发&lt;/li&gt;
&lt;/ul&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;话休絮烦，本文便按照如下图所示的源码结构，重点解读一下π的整个源码 「&lt;em&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt; π0及π0-FAST的GitHub地址：github.***/Physical-Intelligence/openpi&lt;/span&gt;&lt;/em&gt;」&lt;/p&gt; 
 
&lt;ol&gt;
&lt;li&gt;π0的源码结构非常清晰、可读性高，不愧是成熟的商业化公司，是我司七月的学习榜样之一&lt;br&gt; 另，我在解读时，除了尽可能像解读iDP3那样，比如特意在分析代码文件之前，贴一下对应的代码结构截图——避免只是堆砌代码，我还会尽可能把模块之间、模块内部的函数之间彼此的联系及互相调用的关系 都阐述出来&lt;br&gt; &lt;br&gt; 如此，不但&lt;strong&gt;从宏观上做到一目了然(&lt;/strong&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;&lt;em&gt;注意&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;em&gt;，本文按照上图π0的代码结构，先解读&lt;strong&gt;src模块下的&lt;u&gt;model-对应下文第一部分、policy-对应下文第二部分、training-对应下文第三部分，第四部分&lt;/u&gt;&lt;/strong&gt;则解读图中src上面的packages/openpi-&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#1a439c&quot;&gt;&lt;em&gt;&lt;strong&gt;c&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;em&gt;lient，以及&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#1a439c&quot;&gt;&lt;em&gt;&lt;strong&gt;s&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;em&gt;cripts&lt;/em&gt;&lt;/span&gt;&lt;strong&gt;)，更从微观上做到抽丝剥茧&lt;/strong&gt;，看到彼此的联系与调用关系&lt;/li&gt;
&lt;li&gt;我身边的很多朋友目前都在做π0的微调及二次开发，相信本文无论对我身边的朋友，还是对更多人的学习与工作，都会起到比较大的提升&lt;br&gt; PS，​有兴趣或也在对π0做微调的，欢迎私我一两句自我简介(&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;比如在哪个公司做什么，或在哪个高校研几什么专业&lt;/em&gt;&lt;/span&gt;)，邀请进：『&lt;strong&gt;七月具身：π0复现微调交流群&lt;/strong&gt;』&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86%20%E6%A0%B8%E5%BF%83%E6%A8%A1%E5%9D%97src%E4%B8%8Bmodels%E7%9A%84%E5%85%A8%E9%9D%A2%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%A3%E8%AF%BB&quot; name=&quot;%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86%20%E6%A0%B8%E5%BF%83%E6%A8%A1%E5%9D%97src%E4%B8%8Bmodels%E7%9A%84%E5%85%A8%E9%9D%A2%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%A3%E8%AF%BB&quot;&gt;第一部分 π0模型架构的实现：src下models的全面分析与解读&lt;/h2&gt; 
&lt;p&gt;接下来，我们来看核心src下的各个模块，首先是其中的&lt;strong&gt;src/openpi/models&lt;/strong&gt;&lt;/p&gt; 
 
&lt;h3 id=&quot;1.1%C2%A0models%2Fmodel.py%EF%BC%9A%E6%A0%B8%E5%BF%83%E5%9F%BA%E7%A1%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E5%AE%9A%E4%B9%89&quot; name=&quot;1.1%C2%A0models%2Fmodel.py%EF%BC%9A%E6%A0%B8%E5%BF%83%E5%9F%BA%E7%A1%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E5%AE%9A%E4%B9%89&quot;&gt;1.1 models/model.py：核心基础模型的定义&lt;/h3&gt; 
&lt;p&gt;这是模型框架的核心文件，定义了基础的抽象类和数据结构：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`BaseModelConfig`: 所有模型配置的抽象基类&lt;/li&gt;
&lt;li&gt;`BaseModel`: 所有模型实现的抽象基类&lt;/li&gt;
&lt;li&gt;`Observation`: 保存模型输入的数据类&lt;/li&gt;
&lt;li&gt;`Actions`: 定义动作数据格式&lt;/li&gt;
&lt;li&gt;提供了通用功能如`preprocess_observation`和`restore_params`&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;1.1.1 基础组件和关键常量&lt;/h4&gt; 
&lt;p&gt;首先是模型类型枚举，定义了两种支持的模型类型：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`PI0`：标准PI0模型&lt;/li&gt;
&lt;li&gt;`PI0_FAST`：自回归版PI0模型&lt;/li&gt;
&lt;/ol&gt; 
&lt;pre&gt;&lt;code&gt;class ModelType(enum.Enum):
    &quot;&quot;&quot;Supported model types.&quot;&quot;&quot;

    PI0 = &quot;pi0&quot;
    PI0_FAST = &quot;pi0_fast&quot;&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;接下来是 图像输入配置，定义了模型期望的图像输入的键名。这表明模型设计为同时接收三个视角的图像：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;一个基础视图（机器人环境的全局视图）&lt;/li&gt;
&lt;li&gt;左手腕视图（来自左手腕摄像头）&lt;/li&gt;
&lt;li&gt;右手腕视图（来自右手腕摄像头）&lt;/li&gt;
&lt;/ol&gt; 
&lt;pre&gt;&lt;code&gt;# The model always expects these images
IMAGE_KEYS = (
    &quot;base_0_rgb&quot;,
    &quot;left_wrist_0_rgb&quot;,
    &quot;right_wrist_0_rgb&quot;,
)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;再其次，是图像分辨率设置——定义了模型处理图像的标准分辨率为224×224像素&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;# This may need change if we release a small model.
IMAGE_RESOLUTION = (224, 224)&lt;/code&gt;&lt;/pre&gt; 
&lt;h4 name=&quot;2.1%20models%2Fpi0.py%E7%9A%84%E5%AE%9E%E7%8E%B0&quot;&gt;1.1.2 `Observation` 类与Actions类型的详解&lt;/h4&gt; 
&lt;p&gt;`Observation` 类是 OpenPI 框架中的一个核心数据结构，用于存储和管理模型的输入数据&lt;/p&gt; 
&lt;p&gt;首先，它包含了机器人感知系统收集的所有必要信息：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;图像数据 (`images`) &lt;pre&gt;&lt;code&gt;class Observation(Generic[ArrayT]):
    &quot;&quot;&quot;Holds observations, i.e., inputs to the model.

    See `Observation.from_dict` to see the expected dictionary form. This is the format
    that should be produced by the data transforms.
    &quot;&quot;&quot;

    # Images, in [-1, 1] float32.
    images: dict[str, at.Float[ArrayT, &quot;*b h w c&quot;]]&lt;/code&gt;&lt;/pre&gt; 类型：`dict[str, at.Float[ArrayT, &quot;*b h w c&quot;]]&lt;br&gt; 用途：存储多个摄像头视角的图像数据&lt;br&gt; 格式：浮点数数组，范围在 [-1, 1] 之间&lt;br&gt; 维度：`*b` 表示任意批量维度，`h` 和 `w` 是图像高度和宽度，`c` 是颜色通道数&lt;/li&gt;
&lt;li&gt;图像掩码 (`image_masks`) &lt;pre&gt;&lt;code&gt;    # Image masks, with same keys as images.
    image_masks: dict[str, at.Bool[ArrayT, &quot;*b&quot;]]&lt;/code&gt;&lt;/pre&gt; 类型：`dict[str, at.Bool[ArrayT, &quot;*b&quot;]]`&lt;br&gt; 用途：标记对应的图像是否有效&lt;br&gt; 格式：布尔值数组&lt;br&gt; 维度：与图像批量维度相同&lt;/li&gt;
&lt;li&gt;机器人状态 (`state`) &lt;pre&gt;&lt;code&gt;    # Low-dimensional robot state.
    state: at.Float[ArrayT, &quot;*b s&quot;]&lt;/code&gt;&lt;/pre&gt; 类型：`at.Float[ArrayT, &quot;*b s&quot;]`&lt;br&gt; 用途：存储低维度的机器人状态向量&lt;br&gt; 维度：`*b` 表示批量维度，`s` 表示状态向量维度&lt;/li&gt;
&lt;li&gt;语言提示相关字段&lt;br&gt; `tokenized_prompt`：已经tokenized的语言提示 &lt;pre&gt;&lt;code&gt;    # Tokenized prompt.
    tokenized_prompt: at.Int[ArrayT, &quot;*b l&quot;] | None = None&lt;/code&gt;&lt;/pre&gt; `tokenized_prompt_mask`：语言提示的掩码 &lt;pre&gt;&lt;code&gt;    # Tokenized prompt mask.
    tokenized_prompt_mask: at.Bool[ArrayT, &quot;*b l&quot;] | None = None&lt;/code&gt;&lt;/pre&gt; 当然了，两者都是可选字段（可以为 `None`）&lt;/li&gt;
&lt;li&gt;PI0-FAST 模型特有字段&lt;br&gt; `token_ar_mask`：自回归模型的标记掩码 &lt;pre&gt;&lt;code&gt;    # Token auto-regressive mask (for FAST autoregressive model).
    token_ar_mask: at.Int[ArrayT, &quot;*b l&quot;] | None = None&lt;/code&gt;&lt;/pre&gt; `token_loss_mask`：损失计算的标记掩码 &lt;pre&gt;&lt;code&gt;    # Token loss mask (for FAST autoregressive model).
    token_loss_mask: at.Bool[ArrayT, &quot;*b l&quot;] | None = None&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;接下来，定义了`from_dict` 方法，用于从非结构化的字典数据创建 `Observation` 对象：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;数据验证：确保 `tokenized_prompt` 和 `tokenized_prompt_mask` 要么同时存在，要么同时不存在 &lt;pre&gt;&lt;code&gt;    def from_dict(cls, data: at.PyTree[ArrayT]) -&amp;gt; &quot;Observation[ArrayT]&quot;:
        &quot;&quot;&quot;This method defines the mapping between unstructured data (i.e., nested dict) to the structured Observation format.&quot;&quot;&quot;
        # Ensure that tokenized_prompt and tokenized_prompt_mask are provided together.
        if (&quot;tokenized_prompt&quot; in data) != (&quot;tokenized_prompt_mask&quot; in data):
            raise ValueError(&quot;tokenized_prompt and tokenized_prompt_mask must be provided together.&quot;)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;图像格式转换：如果输入图像是 `uint8` 格式（0-255 范围），自动转换为 `float32` 格式（范围 [-1, 1]） &lt;pre&gt;&lt;code&gt;        # If images are uint8, convert them to [-1, 1] float32.
        for key in data[&quot;image&quot;]:
            if data[&quot;image&quot;][key].dtype == np.uint8:
                data[&quot;image&quot;][key] = data[&quot;image&quot;][key].astype(np.float32) / 255.0 * 2.0 - 1.0&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;结构化数据创建：从字典数据创建结构化的 `Observation` 对象 &lt;pre&gt;&lt;code&gt;        return cls(
            images=data[&quot;image&quot;],
            image_masks=data[&quot;image_mask&quot;],
            state=data[&quot;state&quot;],
            tokenized_prompt=data.get(&quot;tokenized_prompt&quot;),
            tokenized_prompt_mask=data.get(&quot;tokenized_prompt_mask&quot;),
            token_ar_mask=data.get(&quot;token_ar_mask&quot;),
            token_loss_mask=data.get(&quot;token_loss_mask&quot;),
        )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;再接下来，又定义了`to_dict` 方法，将 `Observation` 对象转换回非结构化的字典格式：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;使用 `dataclasses.asdict()` 将数据类转换为字典 &lt;pre&gt;&lt;code&gt;    def to_dict(self) -&amp;gt; at.PyTree[ArrayT]:
        &quot;&quot;&quot;Convert the Observation to a nested dict.&quot;&quot;&quot;
        result = dataclasses.asdict(self)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;重命名字段以符合原始数据格式约定（`images` → `image`，`image_masks` → `image_mask`） &lt;pre&gt;&lt;code&gt;        result[&quot;image&quot;] = result.pop(&quot;images&quot;)
        result[&quot;image_mask&quot;] = result.pop(&quot;image_masks&quot;)
        return result&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;最后，在类外定义了 `Actions` 类型，用于表示模型的输出动作：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;# Defines the format of the actions. This field is included as &quot;actions&quot; inside the dictionary
# produced by the data transforms.
Actions = at.Float[ArrayT, &quot;*b ah ad&quot;]&lt;/code&gt;&lt;/pre&gt; 
&lt;ul&gt;
&lt;li&gt;类型：`at.Float[ArrayT, &quot;*b ah ad&quot;]`&lt;/li&gt;
&lt;li&gt;维度：`*b` 表示批量维度，`ah` 表示动作时间步长，`ad` 表示每个动作的维度&lt;/li&gt;
&lt;/ul&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;一朋友在我组建的『七月具身：π0复现微调交流群』问了个比较细节的问题，即&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;我想采集自己的数据来微调这个openpi，然后在采自己的数据时，我的action到底应该采什么（如果采当前帧末端位姿的话，和state有什么区别，只是差个fk而已，不是冗余了么）&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
 &lt;p&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;真正送到模型训练的时候，action又是什么，有大佬可以解决一下吗&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
 &lt;hr&gt; 
 &lt;p&gt;根据OpenPI的代码结构，state和action在robotics任务中具有不同的含义：&lt;/p&gt; 
 &lt;p&gt;&lt;span style=&quot;color:#956fe7&quot;&gt;State (状态)&lt;/span&gt;，代表机器人当前的状态信息，包括：&lt;/p&gt; 
 &lt;ul&gt;
&lt;li&gt;机器人当前的配置，比如关节角度、末端执行器位置等&lt;/li&gt;
&lt;li&gt;末端执行器(end-effector)的位置和方向&lt;/li&gt;
&lt;li&gt;可能还包括物体的状态、环境信息等&lt;/li&gt;
&lt;/ul&gt; 
 &lt;p&gt;如果只采集末端位姿，确实与状态信息存在冗余，只是差一步FK(正向运动学)计算。实际上，有效的&lt;strong&gt;&lt;span style=&quot;color:#511b78&quot;&gt;Action (动作)&lt;/span&gt;&lt;/strong&gt;代表机器人应该执行的下一步控制命令(告诉机器人如何移动)——通常是从当前状态到下一个目标状态的转换，可能是：&lt;/p&gt; 
 &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;关节控制&lt;/strong&gt;&lt;br&gt; 表示目标关节角度，或关节角度的增量变化(delta)&lt;br&gt; &lt;em&gt;说白了，&lt;span style=&quot;color:#956fe7&quot;&gt;state描述：我在哪里&lt;/span&gt;，&lt;strong&gt;&lt;span style=&quot;color:#511b78&quot;&gt;action描述：我要去哪里&lt;/span&gt;&lt;/strong&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;相对位移/速度&lt;/strong&gt;&lt;br&gt; 末端位置到目标位置(target position)的增量变化，和方向&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;控制信号&lt;/strong&gt;&lt;br&gt; 直接发送给执行器的命令，或力矩&lt;/li&gt;
&lt;/ul&gt; 
&lt;/blockquote&gt; 
&lt;h4&gt;1.1.3 preprocess_observation&lt;/h4&gt; 
&lt;h4&gt;1.1.4 BaseModelConfig(abc.ABC)&lt;/h4&gt; 
&lt;h4&gt;1.1.5 class BaseModel(nnx.Module, abc.ABC)&lt;/h4&gt; 
&lt;h4&gt;1.1.6 restore_params&lt;/h4&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;h3 id=&quot;2.1%20models%2Fpi0.py%E7%9A%84%E5%AE%9E%E7%8E%B0&quot; name=&quot;2.1%20models%2Fpi0.py%E7%9A%84%E5%AE%9E%E7%8E%B0&quot;&gt;1.2 models/pi0.py的实现&lt;/h3&gt; 
&lt;p&gt;Pi0是一个多模态扩散模型：继承自`BaseModel`，使用SigLIP处理视觉输入、使用Gemma处理语言输入，实现了基于扩散的动作生成系统，且包含`***pute_loss`和`sample_actions`方法的实现&lt;/p&gt; 
&lt;p&gt;总之，Pi0结合了多模态输入(图像和文本)来生成机器人动作序列。下面是对代码的详细解析：&lt;/p&gt; 
&lt;h4 id=&quot;2.1.1%20make_attn_mask%EF%BC%9A%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%8E%A9%E7%A0%81%E7%94%9F%E6%88%90%E5%87%BD%E6%95%B0&quot; name=&quot;2.1.1%20make_attn_mask%EF%BC%9A%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%8E%A9%E7%A0%81%E7%94%9F%E6%88%90%E5%87%BD%E6%95%B0&quot;&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.1 make_attn_mask：注意力掩码生成函数&lt;/span&gt;&lt;/h4&gt; 
&lt;p&gt;这个函数生成transformer中使用的注意力掩码，控制 token 之间的注意力流动方式&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;def make_attn_mask(input_mask, mask_ar):
    &quot;&quot;&quot;
    从big_vision项目改编的注意力掩码生成函数
    
    Token可以关注那些累积mask_ar小于等于自己的有效输入token。
    这样`mask_ar` bool[?B, N]可用于设置几种类型的注意力，例如：
    
      [[1 1 1 1 1 1]]: 纯因果注意力。
    
      [[0 0 0 1 1 1]]: 前缀语言模型注意力。前3个token之间可以互相关注，
                      后3个token有因果注意力。第一个条目也可以是1，不改变行为。
    
      [[1 0 1 0 1 0 0 1 0 0]]: 4个块之间的因果注意力。一个块的token可以
                              关注所有之前的块和同一块内的所有token。
    
    参数:
      input_mask: bool[B, N] 如果是输入的一部分则为true，如果是填充则为false
      mask_ar: bool[?B, N] 如果前面的token不能依赖于它则为true，
               如果它共享与前一个token相同的注意力掩码则为false
    &quot;&quot;&quot;

    # 将mask_ar广播到与input_mask相同的形状
    mask_ar = jnp.broadcast_to(mask_ar, input_mask.shape)  

    # 计算mask_ar在序列维度上的累积和
    cumsum = jnp.cumsum(mask_ar, axis=1)  

    # 创建注意力掩码：当目标位置的累积值&amp;lt;=查询位置的累积值时，允许注意力流动
    attn_mask = cumsum[:, None, :] &amp;lt;= cumsum[:, :, None]  

    # 创建有效掩码：只有有效的输入位置之间才能有注意力
    valid_mask = input_mask[:, None, :] * input_mask[:, :, None]  

    # 结合注意力掩码和有效掩码
    return jnp.logical_and(attn_mask, valid_mask)  &lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;它支持多种注意力模式：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;纯因果注意力（每个 token 只能关注自己和之前的 token）&lt;/li&gt;
&lt;li&gt;前缀语言模型注意力（允许前缀内部自由注意，后缀部分使用因果注意力）&lt;/li&gt;
&lt;li&gt;块状因果注意力（在块内自由注意，块之间是因果的）&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;2.1.2%20posemb_sincos%EF%BC%9A%E4%BD%8D%E7%BD%AE%E7%BC%96%E7%A0%81%E5%87%BD%E6%95%B0&quot; name=&quot;2.1.2%20posemb_sincos%EF%BC%9A%E4%BD%8D%E7%BD%AE%E7%BC%96%E7%A0%81%E5%87%BD%E6%95%B0&quot;&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.2 posemb_sincos：位置编码函数&lt;/span&gt;&lt;/h4&gt; 
&lt;p&gt;使用正弦余弦函数实现位置编码&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;def posemb_sincos(
    pos: at.Real[at.Array, Any], embedding_dim: int, min_period: float, max_period: float
) -&amp;gt; at.Float[at.Array, f&quot;b {embedding_dim}&quot;]:
    &quot;&quot;&quot;计算标量位置的正弦余弦位置嵌入向量&quot;&quot;&quot;
    if embedding_dim % 2 != 0:      # 检查嵌入维度是否为偶数
        raise ValueError(f&quot;embedding_dim ({embedding_dim}) must be divisible by 2&quot;)

    fraction = jnp.linspace(0.0, 1.0, embedding_dim // 2)  # 创建均匀分布的分数值
    period = min_period * (max_period / min_period) ** fraction  # 计算周期值，对数空间中均匀分布
    sinusoid_input = jnp.einsum(
        &quot;i,j-&amp;gt;ij&quot;,
        pos,
        1.0 / period * 2 * jnp.pi,                      # 计算角频率
        precision=jax.lax.Precision.HIGHEST,            # 使用最高精度进行计算
    )

    # 连接sin和cos值，形成完整的位置编码
    return jnp.concatenate([jnp.sin(sinusoid_input), jnp.cos(sinusoid_input)], axis=-1)&lt;/code&gt;&lt;/pre&gt; 
&lt;h4 id=&quot;2.1.3%20class%20Pi0Config%EF%BC%9A%E5%90%ABinputs_spec%E3%80%81get_freeze_filter&quot; name=&quot;2.1.3%20class%20Pi0Config%EF%BC%9A%E5%90%ABinputs_spec%E3%80%81get_freeze_filter&quot;&gt;
&lt;span style=&quot;color:null&quot;&gt;1.2.3 &lt;/span&gt;class Pi0Config&lt;span style=&quot;color:null&quot;&gt;：含inputs_spec、get_freeze_filter&lt;/span&gt;
&lt;/h4&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;Pi0Config这个类中，&lt;span style=&quot;color:null&quot;&gt;定义了&lt;/span&gt;&lt;/p&gt; 
 &lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color:null&quot;&gt;动作专家底层结构gemma_300m&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:null&quot;&gt;inputs_spec：&lt;/span&gt;π0模型本身接收的输入数据格式&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#fe2c24&quot;&gt;get_freeze_filter&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;(决定对VLM和action expect的哪部分微调，还是都微调)&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt; 
&lt;/blockquote&gt; 
&lt;h5 id=&quot;2.1.3.1%C2%A0%E6%A8%A1%E5%9E%8B%E9%85%8D%E7%BD%AE%E5%8F%82%E6%95%B0%E7%9A%84%E5%AE%9A%E4%B9%89&quot; name=&quot;2.1.3.1%C2%A0%E6%A8%A1%E5%9E%8B%E9%85%8D%E7%BD%AE%E5%8F%82%E6%95%B0%E7%9A%84%E5%AE%9A%E4%B9%89&quot;&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.3.1 模型配置参数的定义&lt;/span&gt;&lt;/h5&gt; 
&lt;p&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;strong&gt;首先&lt;/strong&gt;，这个类定义了模型的配置参数&lt;/span&gt;，比如PaLI-Gemma 变体：`gemma_2b，尤其值得注意的是&lt;span style=&quot;color:#b95514&quot;&gt;在本π0的官方实现中，动作专家的底层结构用的&lt;strong&gt;300M大小的gemma模型变体&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;class Pi0Config(_model.BaseModelConfig):
    dtype: str = &quot;bfloat16&quot;  # 设置数据类型为bfloat16
    paligemma_variant: _gemma.Variant = &quot;gemma_2b&quot;          # 设置PaLI-Gemma变体为2B参数版本
    action_expert_variant: _gemma.Variant = &quot;gemma_300m&quot;    # 设置动作专家为gemma的300M变体版本

    # 设置模型特定的默认值
    action_dim: int = 32          # 设置动作维度为32
    action_horizon: int = 50      # 设置动作序列长度为50步
    max_token_len: int = 48       # 设置最大token长度为48&lt;/code&gt;&lt;/pre&gt; 
&lt;h5 id=&quot;2.1.3.2%C2%A0inputs_spec%EF%BC%9A%E5%AE%9A%E4%B9%89%E4%BA%86%CF%800%E6%A8%A1%E5%9E%8B%E6%9C%AC%E8%BA%AB%E6%8E%A5%E6%94%B6%E7%9A%84%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F%E2%80%8B%E7%BC%96%E8%BE%91&quot; name=&quot;2.1.3.2%C2%A0inputs_spec%EF%BC%9A%E5%AE%9A%E4%B9%89%E4%BA%86%CF%800%E6%A8%A1%E5%9E%8B%E6%9C%AC%E8%BA%AB%E6%8E%A5%E6%94%B6%E7%9A%84%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F%E2%80%8B%E7%BC%96%E8%BE%91&quot;&gt;
&lt;span style=&quot;color:null&quot;&gt;1.2.3.2 inputs_spec：定义了π0模型本身接收的输入数据格式&lt;/span&gt;
&lt;/h5&gt; 
&lt;p&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;strong&gt;其次&lt;/strong&gt;，通过inputs_spec函数定义了π0模型本身接收的输入数据格式，&lt;/span&gt;函数采用关键字参数 `batch_size`（默认为1），返回一个包含观察规格和动作规格的元组&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;def inputs_spec(self, *, batch_size: int = 1) -&amp;gt; Tuple[Type[_model.Observation], Type[_model.Actions]]&lt;/code&gt;&lt;/pre&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;span style=&quot;color:null&quot;&gt;其支持多种输入，比如&lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#1c7892&quot;&gt;视觉输入(三个不同视角的RGB图像)&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;、&lt;/span&gt;&lt;span style=&quot;color:#79c6cd&quot;&gt;语言输入(分词后的文本prompt)&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;、&lt;/span&gt;&lt;span style=&quot;color:#1c7331&quot;&gt;状态输入(当前机器人状态)&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:null&quot;&gt;输出上&lt;/span&gt;&lt;br&gt; &lt;strong&gt;&lt;span style=&quot;color:#1c7331&quot;&gt;则是一个时序动作序列(包含50个连续的动作向量，每个动作向量有32个维度，可能对应关节角度或其他控制信号)&lt;/span&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;具体而言该函数进行如下4个操作&lt;br&gt; &lt;strong&gt;&lt;span style=&quot;color:null&quot;&gt;一、创建图像规格&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        image_spec = jax.ShapeDtypeStruct([batch_size, *_model.IMAGE_RESOLUTION, 3], jnp.float32)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其中的&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`[batch_size, *_model.IMAGE_RESOLUTION, 3]` 定义了图像张量的形状：比如&lt;br&gt;   批次大小&lt;br&gt;   图像分辨率（&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;从 `_model.IMAGE_RESOLUTION` 获取，可能是如 [224, 224] 这样的值&lt;/em&gt;&lt;/span&gt;）&lt;br&gt;   3 个颜色通道 (RGB)&lt;/li&gt;
&lt;li&gt;`jnp.float32` 指定了数据类型为 32 位浮点数&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;strong&gt;二、创建图像掩码规格&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        image_mask_spec = jax.ShapeDtypeStruct([batch_size], jnp.bool_)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其定义了图像掩码规格，每个批次中的每个图像都有一个布尔值，这个掩码用于指示哪些图像是有效的（`True`）或无效的（`False`）&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;u&gt;三、创建观察规格&lt;/u&gt;：包含视觉输入、机器人状态、指令输入&lt;/strong&gt;&lt;br&gt; `at.disable_typechecking()` 临时禁用类型检查，可能是因为这里创建的是类型规格而不是实际的数据，且观察规格包含多个组件：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;多视角图像&lt;br&gt; base_0_rgb: 机器人底座/身体视角的RGB图像&lt;br&gt; left_wrist_0_rgb: 左手腕视角的RGB图像&lt;br&gt; right_wrist_0_rgb: 右手腕视角的RGB图像 &lt;pre&gt;&lt;code&gt;        with at.disable_typechecking():
            observation_spec = _model.Observation(
                images={
                    &quot;base_0_rgb&quot;: image_spec,
                    &quot;left_wrist_0_rgb&quot;: image_spec,
                    &quot;right_wrist_0_rgb&quot;: image_spec,
                },&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;图像掩码&lt;br&gt; 对应每个视角图像的有效性掩码&lt;/li&gt;
&lt;li&gt;机器人状态：&lt;br&gt; 形状为 `[batch_size, self.action_dim]` 的浮点数张量，其中的`self.action_dim` 默认为32，表示状态向量的维度 &lt;pre&gt;&lt;code&gt;                state=jax.ShapeDtypeStruct([batch_size, self.action_dim], jnp.float32),&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;分词后的文本prompt&lt;br&gt; 形状为 `[batch_size, self.max_token_len]` 的整数张量&lt;br&gt; `self.max_token_len` 默认为48，表示最大token数量&lt;br&gt; 数据类型为 `jnp.int32`，表示token ID&lt;/li&gt;
&lt;li&gt;提示掩码&lt;br&gt; 与分词提示相同形状的布尔张量，用于指示哪些位置有有效的token &lt;pre&gt;&lt;code&gt;                state=jax.ShapeDtypeStruct([batch_size, self.action_dim], jnp.float32),
                tokenized_prompt=jax.ShapeDtypeStruct([batch_size, self.max_token_len], jnp.int32),
                tokenized_prompt_mask=jax.ShapeDtypeStruct([batch_size, self.max_token_len], bool),
            )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;strong&gt;四、创建动作规格&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        action_spec = jax.ShapeDtypeStruct([batch_size, self.action_horizon, self.action_dim], jnp.float32)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其定义了动作数据的形状和类型：&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;`batch_size`: 批次大小&lt;/li&gt;
&lt;li&gt;`self.action_horizon`: 动作序列长度，默认为50&lt;/li&gt;
&lt;li&gt; `self.action_dim`: 每个动作的维度，默认为32&lt;/li&gt;
&lt;li&gt;`jnp.float32` 指定了数据类型为32位浮点数&lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;然后返回&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        return observation_spec, action_spec&lt;/code&gt;&lt;/pre&gt; 
&lt;h5 id=&quot;2.1.3.3%C2%A0get_freeze_filter%EF%BC%9A%E9%92%88%E5%AF%B9%E6%98%AF%E5%90%A6LoRA%E7%9A%84%E5%A4%84%E7%90%86&quot; name=&quot;2.1.3.3%C2%A0get_freeze_filter%EF%BC%9A%E9%92%88%E5%AF%B9%E6%98%AF%E5%90%A6LoRA%E7%9A%84%E5%A4%84%E7%90%86&quot;&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.3.3 get_freeze_filter：参数冻结器，包含谁则相当于谁被冻结/过滤&lt;/span&gt;&lt;/h5&gt; 
&lt;p&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;strong&gt;此外&lt;/strong&gt;，该配置类还实现了get_freeze_filter这个函数&lt;/span&gt;&lt;span style=&quot;color:#4d4d4d&quot;&gt;，&lt;/span&gt;作用是如果选择LoRA微调(冻结原始预训练模型的参数，只更新新添加的低秩适应层参数)，则需要对模型中的某些参数做冻结&lt;/p&gt; 
&lt;p&gt;三种可能的情况：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;只对 PaLI-Gemma 使用 LoRA&lt;/strong&gt;&lt;br&gt; 意味着只冻结 Gemma 原始参数，然后排除动作专家原始参数，微调Gemma原始参数之外的少量LoRA部分&lt;br&gt; &lt;br&gt; &lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;注意&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;  首先，不微调π0，也有指令跟随能力&lt;br&gt;   其次，对π0的语言模型部分 使用“指令跟随数据集”做lora微调，不是说 让其丧失指令跟随能力，而是「lora微调对指令跟随能力的加强」作用相对没很大&lt;br&gt; 「&lt;/em&gt;&lt;/span&gt;&lt;em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;关于什么是LoRA，详见此文《&lt;/span&gt;&lt;em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;LLM高效参数微调方法：从Prefix Tuning、Prompt Tuning、P-Tuning V1/V2到LoRA、QLoRA(含对模型量化的解释)&lt;/span&gt;&lt;/em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;》的第4部分&lt;/span&gt;&lt;/em&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;」&lt;br&gt; 毕竟lora微调的本质是 原始参数冻结，而是微调「两个可以近似原矩阵的两个小矩阵」参数&lt;br&gt;   且为免歧义，再强调一句：lora微调 也是有效的，有时甚至可以逼近全参微调&lt;/em&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;只对动作专家使用 LoRA&lt;/strong&gt;&lt;br&gt; 意味着只冻结动作专家参数，微调动作专家原始参数之外的少量LoRA部分&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;对两者都使用 LoRA&lt;/strong&gt;&lt;br&gt; 意味着冻结两者的基础参数，微调两者原始参数之外的少量LoRA部分&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;如此，可以选择性地微调模型的特定部分(语言部分或动作预测部分）&lt;/p&gt; 
&lt;p&gt;具体而言，该&lt;span style=&quot;color:null&quot;&gt;get_freeze_filter分为4大阶段&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第一阶段&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;color:null&quot;&gt;，定义函数本身、初始化变量，并创建参数过滤器&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;首先，定义函数 &lt;pre&gt;&lt;code&gt;    def get_freeze_filter(self) -&amp;gt; nnx.filterlib.Filter:
        &quot;&quot;&quot;返回基于模型配置的冻结过滤器&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;其次，初始化变量 &lt;pre&gt;&lt;code&gt;        filters = []      # 初始化过滤器列表
        has_lora = False  # 初始化LoRA标志&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;接着，创建参数过滤器 &lt;pre&gt;&lt;code&gt;        # 匹配所有LLM参数的正则表达式，用于选择 Gemma 语言模型的参数
        gemma_params_filter = nnx_utils.PathRegex(&quot;.*llm.*&quot;)  

        # 匹配动作专家参数的正则表达式
        action_expert_params_filter = nnx_utils.PathRegex(&quot;.*llm.*_1.*&quot;)  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;&lt;u&gt;&lt;strong&gt;&lt;span style=&quot;color:#be191c&quot;&gt;第二阶段&lt;/span&gt;&lt;/strong&gt;&lt;/u&gt;，分情况添加LoRA权重&lt;/p&gt; 
&lt;p&gt;即要么只对语言模型使用LoRA(意味着不对动作专家使用LoRA)，要么只对动作专家使用LoRA&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;即，接下来是对PaLI-Gemma变体的处理&lt;br&gt; 如果&lt;strong&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;只对PaLI-Gemma使用LoRA&lt;/span&gt;&lt;/strong&gt;，则&lt;br&gt;   &lt;u&gt;一方面，所有Gemma的原始参数将被冻结/过滤掉&lt;/u&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;，&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;因为&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;&lt;em&gt;&lt;strong&gt;LoRA&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;&lt;strong&gt;就是只微调原始参数之外的少量LoRA部分&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt; &lt;pre&gt;&lt;code&gt;        # 如果只针对PaLI-Gemma使用LoRA
        if &quot;lora&quot; in self.paligemma_variant:
            # 过滤器列表添加Gemma的原始参数
            filters.append(
                gemma_params_filter,
            )&lt;/code&gt;&lt;/pre&gt;   且&lt;u&gt;二方面，代表动作专家的原始参数不被冻结/过滤&lt;/u&gt;，故&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;过滤器列表不添加动作专家expert原始参数&lt;/em&gt;&lt;/span&gt;，意味着动作专家可能被全参微调而&lt;span style=&quot;color:#9c8ec1&quot;&gt;非被LoRA微调&lt;/span&gt; &lt;pre&gt;&lt;code&gt;            if &quot;lora&quot; not in self.action_expert_variant:
                # 因为只冻结Gemma参数，故过滤器列表不添加动作专家expert的原始参数
                filters.append(
                    nnx.Not(action_expert_params_filter),
                )
            has_lora = True&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;再下来是对动作专家变体的处理，如果对action_expert_variant使用LoRA，则过滤器列表添加动作专家expert的原始参数，而&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;微调&lt;strong&gt;动作专家原始参数&lt;/strong&gt;之外的少量LoRA部分&lt;/em&gt;&lt;/span&gt; &lt;pre&gt;&lt;code&gt;        elif &quot;lora&quot; in self.action_expert_variant:
            # 如果动作专家使用LoRA，则过滤器列表添加动作专家expert的原始参数
            filters.append(
                action_expert_params_filter,
            )
            has_lora = True&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;u&gt;&lt;strong&gt;第三阶段&lt;/strong&gt;&lt;/u&gt;&lt;/span&gt;，针对需要LoRA微调的少量参数处理，以及如果没有需要LoRA微调时的处理&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;如果有需要被LoRA微调的部分，则&lt;u&gt;&lt;strong&gt;过滤器列表里不添加&lt;/strong&gt;原始参数之外的&lt;strong&gt;LoRA相关参数&lt;/strong&gt;&lt;/u&gt;(代表着不被过滤) &lt;pre&gt;&lt;code&gt;        if has_lora:
            # If any lora is used, exclude all lora params.
            filters.append(
                nnx.Not(nnx_utils.PathRegex(&quot;.*lora.*&quot;)),
            )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;如果没有被冻结/过滤的参数，则什么都不需要处理——即默认微调所有参数 &lt;pre&gt;&lt;code&gt;        if not filters:
            return nnx.Nothing&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第四阶段&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;，返回所有需要被冻结/被过滤的参数，这毕竟是&lt;span style=&quot;color:null&quot;&gt;get_freeze_filter&lt;/span&gt;函数本身定义所追求的目标&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        return nnx.All(*filters)&lt;/code&gt;&lt;/pre&gt; 
&lt;blockquote&gt; 
 &lt;p name=&quot;2.1.4%20class%20Pi0%EF%BC%9A%E5%88%9D%E5%A7%8B%E5%8C%96%E3%80%81%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E3%80%81%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%E3%80%81%E6%8E%A8%E7%90%86(%E5%8E%BB%E5%99%AA%E7%94%9F%E6%88%90%E5%8A%A8%E4%BD%9C)&quot;&gt;值得注意的是，也是我之前看到这里思考过的一个问题，即在训练 π0 的动作预测能力时&lt;/p&gt; 
 &lt;ol&gt;
&lt;li name=&quot;2.1.4%20class%20Pi0%EF%BC%9A%E5%88%9D%E5%A7%8B%E5%8C%96%E3%80%81%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E3%80%81%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%E3%80%81%E6%8E%A8%E7%90%86(%E5%8E%BB%E5%99%AA%E7%94%9F%E6%88%90%E5%8A%A8%E4%BD%9C)&quot;&gt;默认会同时调整 VLM 和动作专家的参数&lt;/li&gt;
&lt;li name=&quot;2.1.4%20class%20Pi0%EF%BC%9A%E5%88%9D%E5%A7%8B%E5%8C%96%E3%80%81%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E3%80%81%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%E3%80%81%E6%8E%A8%E7%90%86(%E5%8E%BB%E5%99%AA%E7%94%9F%E6%88%90%E5%8A%A8%E4%BD%9C)&quot;&gt;如果需要只调整动作专家的参数，可以通过修改 `get_freeze_filter` 方法来冻结 VLM 的参数&lt;/li&gt;
&lt;/ol&gt; 
&lt;/blockquote&gt; 
&lt;h4 id=&quot;2.1.4%20class%20Pi0%EF%BC%9A%E5%88%9D%E5%A7%8B%E5%8C%96%E3%80%81%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E3%80%81%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%E3%80%81%E6%8E%A8%E7%90%86(%E5%8E%BB%E5%99%AA%E7%94%9F%E6%88%90%E5%8A%A8%E4%BD%9C)&quot; name=&quot;2.1.4%20class%20Pi0%EF%BC%9A%E5%88%9D%E5%A7%8B%E5%8C%96%E3%80%81%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E3%80%81%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%E3%80%81%E6%8E%A8%E7%90%86(%E5%8E%BB%E5%99%AA%E7%94%9F%E6%88%90%E5%8A%A8%E4%BD%9C)&quot;&gt;
&lt;span style=&quot;color:null&quot;&gt;1.2.4 &lt;/span&gt;class Pi0&lt;span style=&quot;color:null&quot;&gt;：含特征嵌入(embed_prefix/embed_suffix)、损失函数(训练去噪的准确性)、推理(去噪生成动作)&lt;/span&gt;
&lt;/h4&gt; 
&lt;p&gt;核心模型类，继承自 `_model.BaseModel`，实现了：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;多模态输入处理&lt;br&gt; 处理多视角图像（基础视角、左手腕视角、右手腕视角）&lt;br&gt; 处理文本提示（如指令）&lt;br&gt; 处理机器人当前状态&lt;/li&gt;
&lt;li&gt;扩散过程&lt;br&gt; 训练时：将干净动作添加噪声，让模型学习去噪&lt;br&gt; 推理时：从纯噪声开始，逐步降噪生成动作序列&lt;/li&gt;
&lt;li&gt;注意力机制&lt;br&gt; 使用精心设计的注意力掩码控制信息流动&lt;br&gt; 前缀（图像和文本）内部使用全注意力&lt;br&gt; 后缀（状态和动作）使用特殊的注意力模式&lt;/li&gt;
&lt;/ol&gt; 
&lt;h5 id=&quot;2.1.4.1%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%96%B9%E6%B3%95%20%60__init__%60&quot; name=&quot;2.1.4.1%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%96%B9%E6%B3%95%20%60__init__%60&quot;&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.4.1 初始化方法 `__init__`&lt;/span&gt;&lt;/h5&gt; 
&lt;pre&gt;&lt;code&gt;class Pi0(_model.BaseModel):
    def __init__(self, config: Pi0Config, rngs: nnx.Rngs):
        # 初始化基类
        super().__init__(config.action_dim, config.action_horizon, config.max_token_len)
        
        # 获取PaLI-Gemma和动作专家配置
        paligemma_config = _gemma.get_config(config.paligemma_variant)
        action_expert_config = _gemma.get_config(config.action_expert_variant)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其组合了多个核心组件：&lt;/p&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;一个是PaLI-Gemma 模型&lt;/span&gt;：结合了 Gemma 语言模型和 SigLIP 视觉模型&lt;/p&gt; 
 
&lt;ol&gt;
&lt;li&gt;先是对语言模型的初始化 &lt;pre&gt;&lt;code&gt;        # 创建并初始化语言模型
        # TODO: 用NNX重写Gemma，目前使用桥接
        llm = nnx_bridge.ToNNX(
            _gemma.Module(
                configs=[paligemma_config, action_expert_config],  # 配置两个Gemma模型
                embed_dtype=config.dtype,          # 设置嵌入数据类型
            )
        )
        llm.lazy_init(rngs=rngs, method=&quot;init&quot;)    # 延迟初始化LLM&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;然后是对视觉模型的初始化 &lt;pre&gt;&lt;code&gt;        # 创建并初始化图像模型
        img = nnx_bridge.ToNNX(
            _siglip.Module(
                num_classes=paligemma_config.width,  # 设置图像特征维度与语言模型宽度相匹配
                variant=&quot;So400m/14&quot;,  # 使用400M参数SigLIP模型
                pool_type=&quot;none&quot;,  # 不使用池化，保留所有图像token
                scan=True,  # 启用扫描优化
                dtype_mm=config.dtype,  # 设置矩阵乘法数据类型
            )
        )

        # 使用假观察中的图像初始化图像模型
        img.lazy_init(next(iter(config.fake_obs().images.values())), train=False, rngs=rngs)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;最后，把语言模型和视觉模型组合成PaLI-Gemma多模态模型 &lt;pre&gt;&lt;code&gt;        
        # 组合LLM和图像模型为PaLI-Gemma多模态模型
        self.PaliGemma = nnx.Dict(llm=llm, img=img)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;另一个是线性投影层&lt;/span&gt;：用于&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;状态投影 &lt;pre&gt;&lt;code&gt;        # 状态投影层：将机器人状态投影到模型维度
        self.state_proj = nnx.Linear(config.action_dim, action_expert_config.width, rngs=rngs)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;动作投影 &lt;pre&gt;&lt;code&gt;        # 动作输入投影层：将动作投影到模型维度
        self.action_in_proj = nnx.Linear(config.action_dim, action_expert_config.width, rngs=rngs)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;时间-动作混合等 &lt;pre&gt;&lt;code&gt;        # 动作-时间MLP输入层：将连接的动作和时间特征投影到模型维度
        self.action_time_mlp_in = nnx.Linear(2 * action_expert_config.width, action_expert_config.width, rngs=rngs)

        # 动作-时间MLP输出层
        self.action_time_mlp_out = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)

        # 动作输出投影层：将模型输出投影回动作维度
        self.action_out_proj = nnx.Linear(action_expert_config.width, config.action_dim, rngs=rngs)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h5 id=&quot;2.1.4.2%20%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E6%96%B9%E6%B3%95%EF%BC%9Aembed_prefix(%E5%9B%BE%E5%83%8F%E5%92%8C%E6%96%87%E6%9C%AC%E8%BE%93%E5%85%A5)%E3%80%81embed_suffix(%E7%8A%B6%E6%80%81%E5%92%8C%E5%8A%A8%E4%BD%9C%E4%BF%A1%E6%81%AF)%E2%80%8B%E7%BC%96%E8%BE%91&quot; name=&quot;2.1.4.2%20%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E6%96%B9%E6%B3%95%EF%BC%9Aembed_prefix(%E5%9B%BE%E5%83%8F%E5%92%8C%E6%96%87%E6%9C%AC%E8%BE%93%E5%85%A5)%E3%80%81embed_suffix(%E7%8A%B6%E6%80%81%E5%92%8C%E5%8A%A8%E4%BD%9C%E4%BF%A1%E6%81%AF)%E2%80%8B%E7%BC%96%E8%BE%91&quot;&gt;
&lt;span style=&quot;color:null&quot;&gt;1.2.4.2 特征嵌入方法：&lt;/span&gt;&lt;strong&gt;embed_prefix(图像和文本输入)、embed_suffix(状态和动作信息)&lt;/strong&gt;
&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;`embed_prefix`：处理图像和文本输入(&lt;span style=&quot;color:#1c7892&quot;&gt;&lt;em&gt;图像通过SigLip模型编码，文本通过Gemma LLM编码&lt;/em&gt;&lt;/span&gt;)，创建前缀 token，皆为双向注意力，用ar_mask = false表示&lt;/li&gt;
&lt;li&gt;`embed_suffix`：处理机器人状态信息、噪声化的动作信息(&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;em&gt;状态和噪声动作经过线性投影和MLP处理&lt;/em&gt;&lt;/span&gt;)，创建后缀 token&lt;br&gt; 其中&lt;br&gt;   &lt;strong&gt;状态为单个token，和第一个动作token&lt;/strong&gt;均设置为&lt;u&gt;单向注意力，用ar_mask = true表示&lt;/u&gt;&lt;br&gt;   其余动作tokens之间设置为双向注意力，用ar_mask = false表示&lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;首先，对于前者embed_prefix&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    def embed_prefix(
        self, obs: _model.Observation
    ) -&amp;gt; Tuple[at.Float[at.Array, Any], at.Bool[at.Array, Any], at.Bool[at.Array, Any]]:
        &quot;&quot;&quot;嵌入前缀部分（图像和文本）&quot;&quot;&quot;
        input_mask = []           # 初始化输入掩码列表
        ar_mask = []              # 初始化自回归掩码列表
        tokens = []               # 初始化token列表&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其工作流程为&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;图像处理：说白了，就是把图像token化&lt;/strong&gt;&lt;br&gt; 使用SigLip视觉模型处理每个图像，生成图像tokens序列 &lt;pre&gt;&lt;code&gt;        # 嵌入图像
        for name in obs.images:
            # 通过图像模型获取图像token
            image_tokens, _ = self.PaliGemma.img(obs.images[name], train=False)

            tokens.append(image_tokens)      # 添加图像token&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;图像掩码扩展&lt;br&gt; 将图像掩码扩展到与图像tokens相同的序列长度，使用einops.repeat进行形状变换，这些掩码会&lt;strong&gt;指示哪些图像是有效的，而哪些是填充的&lt;/strong&gt; &lt;pre&gt;&lt;code&gt;            # 重复图像掩码以匹配token维度
            input_mask.append(
                einops.repeat(
                    obs.image_masks[name],
                    &quot;b -&amp;gt; b s&quot;,               # 调整形状：批次维度保持不变，添加序列维度
                    s=image_tokens.shape[1],  # 序列长度等于图像token数
                )
            )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;自回归掩码设置&lt;br&gt; 设置图像tokens之间的注意力为双向(False表示双向注意力)，原因在于图像内容通常是非时序性的数据 &lt;pre&gt;&lt;code&gt;            # 图像token之间互相关注（非自回归）
            ar_mask += [False] * image_tokens.shape[1]&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;文本处理&lt;br&gt; 使用LLM模型对文本输入tokenized_inputs进行嵌入 &lt;pre&gt;&lt;code&gt;        # 添加语言（即分词后的输入）
        if obs.tokenized_prompt is not None:
            # 通过语言模型嵌入分词后的提示
            tokenized_inputs = self.PaliGemma.llm(obs.tokenized_prompt, method=&quot;embed&quot;)
            tokens.append(tokenized_inputs)                  # 添加文本token
            input_mask.append(obs.tokenized_prompt_mask)     # 添加提示掩码&lt;/code&gt;&lt;/pre&gt; 且同样设置为双向注意力，相当于&lt;strong&gt;语言token可以关注图像token，图像token反过来亦可关注语言token，最终实现多模态融合&lt;/strong&gt; &lt;pre&gt;&lt;code&gt;            # 图像和语言输入之间完全关注（非自回归）
            ar_mask += [False] * tokenized_inputs.shape[1]&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;最后，连接所有token和掩码，其中包含了&lt;br&gt;   多模态信息的融合表示tokens——图像token和语言token&lt;br&gt;   以及指示哪些token是有效信息的input_mask&lt;br&gt;   和如何在这些token之间进行注意力计算规则的ar_mask——相当于控制信息流动的方向 &lt;pre&gt;&lt;code&gt;        # 连接所有token和掩码
        tokens = jnp.concatenate(tokens, axis=1)    # 在序列维度上连接token
        input_mask = jnp.concatenate(input_mask, axis=1)  # 在序列维度上连接输入掩码
        ar_mask = jnp.array(ar_mask)                # 转换自回归掩码为数组
        
        return tokens, input_mask, ar_mask          # 返回token、输入掩码和自回归掩码&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;顺便，再回顾下此图&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;其次，对于后者embed_suffix&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;p&gt;定义如下，其参数包括obs(一般包含图像和机器人状态)、noisy_actions、timestep&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    def embed_suffix(
        self, obs: _model.Observation, noisy_actions: _model.Actions, timestep: at.Float[at.Array, Any]
    ) -&amp;gt; Tuple[at.Float[at.Array, Any], at.Bool[at.Array, Any], at.Bool[at.Array, Any]]:
        &quot;&quot;&quot;嵌入后缀部分（状态和动作）&quot;&quot;&quot;
        input_mask = []           # 初始化输入掩码列表
        ar_mask = []              # 初始化自回归掩码列表
        tokens = []               # 初始化token列表&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其工作流程为&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;状态处理&lt;br&gt; 将状态信息投影到embedding空间 &lt;pre&gt;&lt;code&gt;        # 添加单个状态token
        state_token = self.state_proj(obs.state)[:, None, :]  # 投影状态并添加序列维度
        tokens.append(state_token)                            # 添加状态token

        # 添加状态掩码（全为1），表示这个状态token是有效的
        input_mask.append(jnp.ones((obs.state.shape[0], 1), dtype=jnp.bool_))  &lt;/code&gt;&lt;/pre&gt; 并设置为单向注意力(True)，表明图像和语言输入不能关注状态信息，因为image/language do not attend to state or actions &lt;pre&gt;&lt;code&gt;        # 图像/语言输入不关注状态或动作（自回归）
        ar_mask += [True]&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;时间步嵌入，使用正弦-余弦位置编码生成时间步嵌入 &lt;pre&gt;&lt;code&gt;       # 使用正弦余弦位置编码嵌入时间步，敏感度范围为[0, 1]
        time_emb = posemb_sincos(timestep, self.action_in_proj.out_features, min_period=4e-3, max_period=4.0)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;动作和时间信息融合，比如通过action_time_tokens连接：「带噪声的动作」和「时间token」 &lt;pre&gt;&lt;code&gt;        # 混合时间步 + 动作信息，使用MLP
        action_tokens = self.action_in_proj(noisy_actions)  # 投影带噪声的动作

        # 重复时间嵌入以匹配动作序列长度
        time_tokens = einops.repeat(time_emb, &quot;b emb -&amp;gt; b s emb&quot;, s=self.action_horizon)

        # 连接动作和时间token
        action_time_tokens = jnp.concatenate([action_tokens, time_tokens], axis=-1)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#be191c&quot;&gt;MLP处理&lt;/span&gt;&lt;br&gt; 使用两层MLP和swish激活函数对「动作和时间的组合表示」进行非线性变换，以进一步融合：(噪声)动作和时间信息 &lt;pre&gt;&lt;code&gt;        # 通过MLP处理
        action_time_tokens = self.action_time_mlp_in(action_time_tokens)   # 输入层
        action_time_tokens = nnx.swish(action_time_tokens)                 # Swish激活函数
        action_time_tokens = self.action_time_mlp_out(action_time_tokens)  # 输出层&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;注意力掩码设置&lt;br&gt; 第一个动作token设置为单向注意力「&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;上面说过了的，&lt;u&gt;单向注意力，用ar_mask = true表示&lt;/u&gt;&lt;/em&gt;&lt;/span&gt;」，其余动作tokens之间设置为双向注意力 &lt;pre&gt;&lt;code&gt;        # 添加动作时间token
        tokens.append(action_time_tokens)

        # 添加掩码（全为1），表示所有动作token都是有效的
        input_mask.append(jnp.ones(action_time_tokens.shape[:2], dtype=jnp.bool_))  

        # 图像/语言/状态输入不关注动作token（动作第一个是自回归的——单向，其余不是——双向）
        ar_mask += [True] + ([False] * (self.action_horizon - 1))&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;最后连接所有token和掩码 &lt;pre&gt;&lt;code&gt;        # 连接所有token和掩码
        tokens = jnp.concatenate(tokens, axis=1)          # 在序列维度上连接token
        input_mask = jnp.concatenate(input_mask, axis=1)  # 在序列维度上连接输入掩码
        ar_mask = jnp.array(ar_mask)        # 转换自回归掩码为数组
        
        return tokens, input_mask, ar_mask  # 返回token、输入掩码和自回归掩码&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h5 id=&quot;2.1.4.3%20%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%20%60***pute_loss%60&quot; name=&quot;2.1.4.3%20%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%20%60***pute_loss%60&quot;&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.4.3 损失函数***pute_loss：训练模型去噪的准确率&lt;/span&gt;&lt;/h5&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span style=&quot;color:null&quot;&gt;总的来讲&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;span style=&quot;color:null&quot;&gt;训练的时候，对其中的「原始动作action」数据加噪，最后去预测所添加的真实噪声，预测噪声的结果为，然后计算预测噪声与真实噪声之间的均方误差&lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#1c7331&quot;&gt;&lt;em&gt;&lt;strong&gt;也就是说，训练时的本质 其实是为了让模型具备生成真正想要动作的能力，以确保在推理时，能得到真正想要动作的能力&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;br&gt; &lt;span style=&quot;color:null&quot;&gt;&lt;em&gt;那可能有同学疑问了，既然通过对&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;原始动作&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;em&gt;加&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#956fe7&quot;&gt;&lt;em&gt;噪&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;em&gt;，然后&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#956fe7&quot;&gt;&lt;em&gt;预测噪声&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;em&gt;，最后&lt;/em&gt;&lt;/span&gt;&lt;strong&gt;&lt;span style=&quot;color:#511b78&quot;&gt;&lt;em&gt;噪声动作&lt;/em&gt;&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;em&gt;减掉预测&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#956fe7&quot;&gt;&lt;em&gt;噪声&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;em&gt; 便是所预测的&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;原始动作&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;em&gt;，那为何不对比实际的原始动作，与所预测的原始动作 是否一致呢&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;其实我之前在此文《图像生成发展起源：从VAE、扩散模型DDPM、DDIM到DETR、ViT、Swin transformer》中的「2.1.1 从扩散模型概念的提出到DDPM(含U-***网络的简介)、DDIM」已经讲了，原因在于&lt;br&gt; 1 对噪声的预测，比对动作的预测更容易，一者 预测噪声收敛更稳定，二者 噪声通常是标准化的，比如高斯噪声的均值为0 方差为1，使得模型预测噪声时不需要适应不同尺度的输出&lt;br&gt; 2 &lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;-prediction 和 -prediction其实理论上也是等价的，毕竟 + &lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt; = &lt;/em&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:null&quot;&gt;如此，便可以在推理的时候，针对一个随机生成的纯噪声，基于observation(包含图像和机器人状态)，逐步去噪生成机器人的动作序列&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;具体而言，&lt;span style=&quot;color:null&quot;&gt;***pute_loss&lt;/span&gt;实现了扩散模型的训练损失计算&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;对输入观察进行预处理，其中&lt;br&gt; preprocess_rng用于观察预处理(比如图像增强等)&lt;br&gt; noise_rng用于生成噪声&lt;br&gt; time_rng用于从beta分布采样时间步 &lt;pre&gt;&lt;code&gt;    def ***pute_loss(
        self, rng: at.KeyArrayLike, observation: _model.Observation, actions: _model.Actions, *, train: bool = False
    ) -&amp;gt; at.Float[at.Array, Any]:
        &quot;&quot;&quot;计算扩散模型的损失函数&quot;&quot;&quot;
        # 分割随机数生成器为三部分，用于不同的随机操作
        preprocess_rng, noise_rng, time_rng = jax.random.split(rng, 3)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;生成随机噪声并采样时间点 t &lt;pre&gt;&lt;code&gt;        # 获取动作的批次形状
        batch_shape = actions.shape[:-2]

        # 生成与动作相同形状的高斯噪声
        noise = jax.random.normal(noise_rng, actions.shape)

        # 从Beta分布采样时间点，范围为[0.001, 1]，Beta(1.5, 1)偏向较低的值
        time = jax.random.beta(time_rng, 1.5, 1, batch_shape) * 0.999 + 0.001

        # 扩展时间维度以匹配动作形状
        time_expanded = time[..., None, None]&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;创建带噪动作序列 x_t，相当于&lt;strong&gt;&lt;span style=&quot;color:#511b78&quot;&gt;x_t是噪声化的动作&lt;/span&gt;&lt;/strong&gt;，随着时间从0到1，&lt;span style=&quot;color:#9c8ec1&quot;&gt;原始动作&lt;/span&gt;&lt;u&gt;&lt;u&gt;&lt;/u&gt;&lt;/u&gt;逐渐添加&lt;span style=&quot;color:#956fe7&quot;&gt;&lt;strong&gt;真实噪声&lt;/strong&gt;&lt;u&gt;&lt;u&gt;&lt;/u&gt;&lt;/u&gt;&lt;/span&gt;，变为&lt;span style=&quot;color:#511b78&quot;&gt;&lt;strong&gt;纯噪声&lt;u&gt;&lt;strong&gt;&lt;u&gt;&lt;/u&gt;&lt;/strong&gt;&lt;/u&gt;&lt;/strong&gt;&lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#956fe7&quot;&gt;&lt;strong&gt;而&lt;strong&gt;&lt;/strong&gt;  代表所加的真实噪声&lt;/strong&gt;&lt;/span&gt;，便是咱们所要预测噪声的ground truth&lt;br&gt; 故&lt;strong&gt;&lt;span style=&quot;color:#956fe7&quot;&gt;所添加的噪声 &lt;strong&gt;&lt;/strong&gt;&lt;/span&gt; &lt;/strong&gt;即  =  &lt;span style=&quot;color:#511b78&quot;&gt;&lt;strong&gt;加满噪声的动作&lt;/strong&gt;&lt;/span&gt; - &lt;span style=&quot;color:#9c8ec1&quot;&gt;原始动作&lt;/span&gt;&lt;/p&gt; &lt;pre&gt;&lt;code&gt;        # 创建带噪声的动作：t * noise + (1-t) * actions
        x_t = time_expanded * noise + (1 - time_expanded) * actions

        # 计算真实噪声减去动作的差异，这是模型需要预测的目标
        u_t = noise - actions&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;嵌入前缀和后缀 &lt;pre&gt;&lt;code&gt;        # 一次性前向传递前缀+后缀
        # 嵌入前缀（图像和文本）
        prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)

        # 嵌入后缀（状态和带噪声的动作）
        suffix_tokens, suffix_mask, suffix_ar_mask = self.embed_suffix(observation, x_t, time)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;构建注意力掩码和位置编码&lt;br&gt; 根据下图&lt;br&gt; &lt;br&gt; 可得 &lt;pre&gt;&lt;code&gt;        # 连接掩码：通过链接前缀和后缀的掩码，从而创建完整的输入掩码
        input_mask = jnp.concatenate([prefix_mask, suffix_mask], axis=1)
        ar_mask = jnp.concatenate([prefix_ar_mask, suffix_ar_mask], axis=0)

        # 创建注意力掩码make_attn_mask，从而控制不同token之间的可见性
        attn_mask = make_attn_mask(input_mask, ar_mask)

        # 计算位置编码
        positions = jnp.cumsum(input_mask, axis=1) - 1&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;模型前向传播，即&lt;span style=&quot;color:#511b78&quot;&gt;&lt;strong&gt;调用PaliGemma进行推理，处理前缀和后缀token&lt;/strong&gt;&lt;/span&gt;&lt;br&gt; 当然了，输出中我们只关注与后缀相关的部分，因为其中包含了我们想要的动作预测的部分 &lt;pre&gt;&lt;code&gt;        # 通过PaLI-Gemma模型处理token
        _, suffix_out = self.PaliGemma.llm(
            [prefix_tokens, suffix_tokens], mask=attn_mask, positions=positions
        )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;预测噪声 &lt;pre&gt;&lt;code&gt;        # 将模型输出投影回动作空间
        v_t = self.action_out_proj(suffix_out[:, -self.action_horizon :])&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;计算预测噪声与实际噪声&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;em&gt;&lt;u&gt;&lt;/u&gt;&lt;/em&gt;&lt;/span&gt;间的均方误差 &lt;pre&gt;&lt;code&gt;        # 返回预测噪声和真实噪声之间的均方误差
        return jnp.mean(jnp.square(v_t - u_t), axis=-1)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;%E9%80%89%E8%AF%BB%20LeRobotDataset%EF%BC%9A%E8%AE%AD%E7%BB%83%E6%95%B0%E6%8D%AE%E9%9B%86%E7%9A%84%E6%9D%A5%E6%BA%90(%E5%8D%B3%E8%AE%AD%E7%BB%83%E6%95%B0%E6%8D%AE%E9%9B%86%E9%95%BF%E4%BB%80%E4%B9%88%E6%A0%B7)&quot; name=&quot;%E9%80%89%E8%AF%BB%20LeRobotDataset%EF%BC%9A%E8%AE%AD%E7%BB%83%E6%95%B0%E6%8D%AE%E9%9B%86%E7%9A%84%E6%9D%A5%E6%BA%90(%E5%8D%B3%E8%AE%AD%E7%BB%83%E6%95%B0%E6%8D%AE%E9%9B%86%E9%95%BF%E4%BB%80%E4%B9%88%E6%A0%B7)&quot;&gt;注解 LeRobotDataset：训练数据集的来源(即训练数据集长什么样)&lt;/h4&gt; 
&lt;p name=&quot;2.1.4.4%20%E6%8E%A8%E7%90%86%E5%87%BD%E6%95%B0%20%60sample_actions%60%EF%BC%9A%E5%9F%BA%E4%BA%8E%E6%89%A9%E6%95%A3%E6%A8%A1%E5%9E%8B%E9%80%86%E5%90%91%E9%87%87%E6%A0%B7%EF%BC%8C%E7%94%9F%E6%88%90%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%8A%A8%E4%BD%9C%E5%BA%8F%E5%88%97&quot;&gt;不知道有没有同学会疑问这段代码里面的数据集 是从哪来的，比如原始动作action 从哪来的，我暂且不管有没有疑惑，假设有人有此疑惑，故我来解释下数据集的来源途径&lt;/p&gt; 
&lt;p&gt;π0主要使用两种数据集：&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;FakeDataset - 生成随机数据用于测试&lt;/li&gt;
&lt;li&gt;LeRobotDataset - 真实的机器人操作数据&lt;/li&gt;
&lt;/ul&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;LeRobotDataset 是一个专为机器人学习设计的数据集格式，来自`lerobot.***mon.datasets.lerobot_dataset`模块。这个数据集包含了训练π0模型所需的观察数据和动作数据，其包含&lt;/p&gt; 
 &lt;ol&gt;
&lt;li&gt;Aloha数据集，侧重双臂协同的精确操作，适合特定任务的模仿学习，比如这个是打开笔帽的任务&lt;/li&gt;
&lt;li&gt;
Libero数据集，注重多样化任务和泛化能力，适合语言引导的通用机器人控制&lt;br&gt; 
&lt;/li&gt;
&lt;/ol&gt; 
 &lt;hr&gt; 
 &lt;p&gt;LeRobotDataset 数据通常包含以下几个关键部分：&lt;/p&gt; 
 &lt;ol&gt;
&lt;li&gt;观察数据 (Observation)&lt;br&gt; 图像数据：来自不同摄像头的图像 &lt;pre&gt;&lt;code&gt;&quot;observation.images.cam_high&quot;
&quot;observation.images.cam_low&quot;
&quot;observation.images.cam_left_wrist&quot;
&quot;observation.images.cam_right_wrist&quot;&lt;/code&gt;&lt;/pre&gt; 状态数据：机器人的关节角度等状态信息 &lt;pre&gt;&lt;code&gt;&quot;observation.state&quot;&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;动作数据 (Actions)&lt;br&gt; 动作序列：每个时间步的机器人动作指令 &lt;pre&gt;&lt;code&gt;&quot;action&quot;&lt;/code&gt;&lt;/pre&gt; 时间戳信息：通过`delta_timestamps`定义的时间间隔&lt;/li&gt;
&lt;li&gt;任务信息&lt;br&gt; 任务描述：可用于生成提示(prompt)&lt;br&gt; 元数据：包括帧率(fps)等信息&lt;/li&gt;
&lt;/ol&gt; 
 &lt;p&gt;数据集示例&lt;/p&gt; 
 &lt;ol&gt;
&lt;li&gt;ALOHA数据集&lt;br&gt; physical-intelligence/aloha_pen_uncap_diverse &lt;pre&gt;&lt;code&gt;{
    &quot;observation&quot;: {
        &quot;images&quot;: {
            &quot;cam_high&quot;: np.ndarray(shape=(3, 224, 224), dtype=np.uint8),
            &quot;cam_left_wrist&quot;: np.ndarray(shape=(3, 224, 224), dtype=np.uint8),
            &quot;cam_right_wrist&quot;: np.ndarray(shape=(3, 224, 224), dtype=np.uint8)
        },
        &quot;state&quot;: np.ndarray(shape=(14,), dtype=np.float32)
    },
    &quot;action&quot;: np.ndarray(shape=(14,), dtype=np.float32),
    &quot;prompt&quot;: &quot;uncap the pen&quot;
}&lt;/code&gt;&lt;/pre&gt; 其中，14维机器人状态向量的含义 &lt;pre&gt;&lt;code&gt;[
    # 左臂关节角度 (6维)
    left_shoulder_pitch,
    left_shoulder_roll,
    left_shoulder_yaw,
    left_elbow_pitch,
    left_elbow_roll,
    left_wrist_pitch,

    # 左手爪状态 (1维)
    left_gripper,

    # 右臂关节角度 (6维)
    right_shoulder_pitch,
    right_shoulder_roll,
    right_shoulder_yaw,
    right_elbow_pitch,
    right_elbow_roll,
    right_wrist_pitch,

    # 右手爪状态 (1维)
    right_gripper
]&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;一个LeRobotDataset的样本可能看起来像这样&lt;br&gt; 比如Libero数据集：physical-intelligence/libero &lt;pre&gt;&lt;code&gt;{
    &quot;observation&quot;: {
        &quot;images&quot;: {
            # 高视角RGB图像，224x224x3
            &quot;cam_high&quot;: np.ndarray(shape=(224, 224, 3), dtype=np.uint8),
            # 低视角RGB图像
            &quot;cam_low&quot;: np.ndarray(shape=(224, 224, 3), dtype=np.uint8),
            # 左手腕视角RGB图像
            &quot;cam_left_wrist&quot;: np.ndarray(shape=(224, 224, 3), dtype=np.uint8),
            # 右手腕视角RGB图像
            &quot;cam_right_wrist&quot;: np.ndarray(shape=(224, 224, 3), dtype=np.uint8)
        },

        # 机器人状态向量，包含关节角度等信息
        &quot;state&quot;: np.ndarray(shape=(14,), dtype=np.float32),  
    },

    # 动作序列，50个时间步，每步14维动作向量
    &quot;actions&quot;: np.ndarray(shape=(50, 14), dtype=np.float32),

    # 任务描述文本
    &quot;prompt&quot;: &quot;fold the towel&quot;
}&lt;/code&gt;&lt;/pre&gt; 再比如 &lt;pre&gt;&lt;code&gt;{
    &quot;observation&quot;: {
        &quot;images&quot;: {
            &quot;cam_high&quot;: &amp;lt;224x224x3 RGB image of robot workspace from above&amp;gt;,
            &quot;cam_left_wrist&quot;: &amp;lt;224x224x3 RGB image from left gripper&amp;gt;,
            &quot;cam_right_wrist&quot;: &amp;lt;224x224x3 RGB image from right gripper&amp;gt;
        },
        &quot;state&quot;: [0.1, -0.5, 0.3, ...],  # 14维机器人关节状态
    },
    &quot;actions&quot;: [
        [0.1, -0.2, 0.3, ...],  # t=0时刻的动作
        [0.15, -0.25, 0.35, ...],  # t=1时刻的动作
        ...  # 共50个时间步
    ],
    &quot;prompt&quot;: &quot;pick up the blue cube and place it in the red bowl&quot;
}&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;真实数据来自`lerobot_dataset`模块，通过以下代码加载——&lt;span style=&quot;color:#511b78&quot;&gt;&lt;em&gt;下文「2.2.2 create_dataset：创建适合训练的数据集」还会详解&lt;/em&gt;&lt;/span&gt;：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;dataset_meta = lerobot_dataset.LeRobotDatasetMetadata(repo_id, local_files_only=data_config.local_files_only)
dataset = lerobot_dataset.LeRobotDataset(
    data_config.repo_id,
    delta_timestamps={
        key: [t / dataset_meta.fps for t in range(model_config.action_horizon)]
        for key in data_config.action_sequence_keys
    },
    local_files_only=data_config.local_files_only,
)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;这里的`repo_id`指向一个特定的数据仓库，是Hugging Face上的数据集或其他存储位置。数据集通过配置文件中的参数指定，例如我们在`config.py`中看到的配置——&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;下文「2.1 配置系统 (config.py)」还会详解&lt;/em&gt;&lt;/span&gt;：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    # Inference Aloha configs.
    #
    TrainConfig(
        name=&quot;pi0_aloha&quot;,
        model=pi0.Pi0Config(),
        data=LeRobotAlohaDataConfig(
            assets=AssetsConfig(asset_id=&quot;trossen&quot;),
        ),
    ),&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;以下是对数据流程总结&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;从LeRobot数据集加载原始数据，包含观察(observation)和动作(action)&lt;/li&gt;
&lt;li&gt;通过数据转换管道处理数据，包括重打包和归一化&lt;/li&gt;
&lt;li&gt;在训练期间，向原始动作添加噪声&lt;/li&gt;
&lt;li&gt;模型学习预测添加的噪声，而不是直接预测原始动作&lt;/li&gt;
&lt;li&gt;在推理时，模型从纯噪声开始，通过迭代去噪过程生成动作序列&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;这种基于扩散的方法允许π0从噪声中逐步精炼动作，最终生成平滑且符合任务要求的机器人动作序列&lt;/p&gt; 
&lt;h5 id=&quot;2.1.4.4%20%E6%8E%A8%E7%90%86%E5%87%BD%E6%95%B0%20%60sample_actions%60%EF%BC%9A%E5%9F%BA%E4%BA%8E%E6%89%A9%E6%95%A3%E6%A8%A1%E5%9E%8B%E9%80%86%E5%90%91%E9%87%87%E6%A0%B7%EF%BC%8C%E7%94%9F%E6%88%90%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%8A%A8%E4%BD%9C%E5%BA%8F%E5%88%97&quot; name=&quot;2.1.4.4%20%E6%8E%A8%E7%90%86%E5%87%BD%E6%95%B0%20%60sample_actions%60%EF%BC%9A%E5%9F%BA%E4%BA%8E%E6%89%A9%E6%95%A3%E6%A8%A1%E5%9E%8B%E9%80%86%E5%90%91%E9%87%87%E6%A0%B7%EF%BC%8C%E7%94%9F%E6%88%90%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%8A%A8%E4%BD%9C%E5%BA%8F%E5%88%97&quot;&gt;
&lt;strong&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.4.4 推理函数 `sample_actions`：基于扩散模型逆向采样(即去噪)，生成机器人动作序列&lt;/span&gt;&lt;/strong&gt; &lt;/h5&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span style=&quot;color:null&quot;&gt;sample_actions&lt;/span&gt;&lt;/strong&gt;函数是Pi0模型的核心推理方法，实现了基于扩散模型的逆向采样过程——&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;说白了 就是去噪，它从纯噪声开始，通过多步骤逐渐&quot;去噪&quot;，最终生成符合条件分布的机器人动作序列&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;函数的核心是一个基于while循环的迭代过程，每一步都使用训练好的神经网络预测从当前噪声化动作到目标动作的方向——&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;从噪声到目标的方向 代表速度场，毕竟咱们去噪的方向得对 不然就去歪了&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;总之，这个函数将观察数据（图像和可选的文本提示）转换为具体的动作轨迹，是模型部署时的主要接口，简言之，其包含以下流程&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;首先从纯噪声开始 (t=1)&lt;/li&gt;
&lt;li&gt;通过重复迭代降噪步骤，逐步将噪声转化为有意义的动作序列&lt;/li&gt;
&lt;li&gt;使用KV缓存优化推理速度&lt;/li&gt;
&lt;li&gt;实现了一个迭代降噪过程&lt;/li&gt;
&lt;li&gt;最终返回完全降噪后的动作序列 x_0&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;具体而言，包含如下步骤&lt;/p&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;u&gt;&lt;strong&gt;第一，初始化&lt;/strong&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;首先，函数对输入观察数据进行预处理，包括标准化图像大小等操作&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;def sample_actions(
    self,
    rng: at.KeyArrayLike,               # 随机数生成器
    observation: _model.Observation,    # 观察输入，包含图像和文本等
    *,
    num_steps: int = 10,                # 扩散过程的步数，默认为10步
) -&amp;gt; _model.Actions:                    # 返回生成的动作序列

    # 对观察数据进行预处理，不进行训练时的数据增强
    observation = _model.preprocess_observation(None, observation, train=False)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;然后&lt;strong&gt;&lt;span style=&quot;color:#4da8ee&quot;&gt;设置时间步长`dt`为负值（因为是从t=1向t=0方向演化），生成初始随机噪声作为起点，且时间上约定：&quot;t=1是噪声，t=0是目标分布&quot;&lt;/span&gt;&lt;/strong&gt;，这是扩散文献中常见的约定，不过与Pi0论文相反&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    # 注意：这里使用扩散模型文献中更常见的约定，t=1是噪声，t=0是目标分布
    # 这与pi0论文相反
    dt = -1.0 / num_steps                       # 计算时间步长，从1到0
    batch_size = observation.state.shape[0]     # 获取批次大小

    # 生成初始噪声，形状为[批次大小, 动作序列长度, 动作维度]
    noise = jax.random.normal(rng, (batch_size, self.action_horizon, self.action_dim))&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;u&gt;&lt;strong&gt;第二，Key-Value缓存初始化&lt;/strong&gt;&lt;/u&gt;&lt;/span&gt;(预计算并存储前缀表示，减少冗余计算)&lt;/p&gt; 
&lt;p&gt;处理观察数据，得到前缀表示和相关掩码&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    # 首先通过前缀的前向传递填充KV缓存
    # 获取前缀的token表示和掩码
    prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)

    # 创建前缀的注意力掩码
    prefix_attn_mask = make_attn_mask(prefix_mask, prefix_ar_mask)

    # 计算位置编码
    positions = jnp.cumsum(prefix_mask, axis=1) - 1&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;然后使用PaliGemma语言模型进行一次前向传递，生成Key-Value缓存（`kv_cache`）——&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;这是一个性能优化：因为前缀部分在整个采样过程中保持不变，预先计算并缓存它们的表示可以避免重复计算&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    # 进行前向传递，获取KV缓存
    _, kv_cache = self.PaliGemma.llm([prefix_tokens, None], mask=prefix_attn_mask, positions=positions)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;u&gt;&lt;strong&gt;&lt;span style=&quot;color:#be191c&quot;&gt;第三，通过step函数构建注意力掩码系统并让PaliGemma做推理&lt;/span&gt;&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;p&gt;核心迭代通过 `jax.lax.while_loop` 实现&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;根据源码&lt;/p&gt; 
  
 &lt;p&gt;可知，该&lt;strong&gt;&lt;em&gt;class Pi0(_model.BaseModel)&lt;/em&gt;&lt;/strong&gt;类的最后两行是&lt;/p&gt; 
 &lt;pre&gt;&lt;code&gt;    # 使用while循环进行迭代采样，从t=1（噪声）开始
    x_0, _ = jax.lax.while_loop(cond, step, (noise, 1.0))

    # 返回最终的去噪结果（生成的动作序列）
    return x_0&lt;/code&gt;&lt;/pre&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;具体而言，包含 `step` 函数和 `cond` 函数，其中，`step` 函数是每次迭代的核心&lt;/p&gt; 
&lt;p&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;首先&lt;/span&gt;，step函数通过 `embed_suffix` 处理当前状态，包括状态信息嵌入、噪声化动作、时间步编码&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    def step(carry):
        &quot;&quot;&quot;定义单步去噪函数&quot;&quot;&quot;
        x_t, time = carry  # carry数组包含当前状态和时间

        # 将时间广播到批次维度，并嵌入后缀（状态和动作）
        suffix_tokens, suffix_mask, suffix_ar_mask = self.embed_suffix(
            observation, x_t, jnp.broadcast_to(time, batch_size)
        )&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;其次&lt;/span&gt;，构建复杂的注意力掩码系统，处理前缀-后缀之间的注意力关系——&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;这个复杂的掩码系统允许后缀token（包括状态和动作）有选择地关注前缀token（图像和文本），实现了条件生成&lt;/em&gt;&lt;/span&gt;，具体而言，其构建了三层注意力掩码：&lt;/p&gt; 
&lt;ul&gt;&lt;li&gt;后缀内部注意力掩码，控制后缀token（状态和动作）之间的注意力关系&lt;/li&gt;&lt;/ul&gt; 
&lt;pre&gt;&lt;code&gt;        # 创建后缀内部的注意力掩码，形状为(批次, 后缀长度, 后缀长度)
        suffix_attn_mask = make_attn_mask(suffix_mask, suffix_ar_mask)&lt;/code&gt;&lt;/pre&gt; 
&lt;ul&gt;&lt;li&gt;前缀-后缀注意力掩码，控制后缀token如何关注前缀token（图像和文本输入）&lt;/li&gt;&lt;/ul&gt; 
&lt;pre&gt;&lt;code&gt;        # 创建后缀对前缀的注意力掩码，形状为(批次, 后缀长度, 前缀长度)
        prefix_attn_mask = einops.repeat(prefix_mask, &quot;b p -&amp;gt; b s p&quot;, s=suffix_tokens.shape[1])&lt;/code&gt;&lt;/pre&gt; 
&lt;ul&gt;&lt;li&gt;完整注意力掩码，将前两个掩码组合，形成完整的注意力控制机制&lt;/li&gt;&lt;/ul&gt; 
&lt;pre&gt;&lt;code&gt;        # 组合掩码，形状为(批次, 后缀长度, 前缀长度+后缀长度)
        # 控制后缀token（生成查询）如何关注完整序列（生成键和值）
        full_attn_mask = jnp.concatenate([prefix_attn_mask, suffix_attn_mask], axis=-1)&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;当然了，过程中还做了形状检查，确保张量维度正确&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        # 验证掩码形状正确
        assert full_attn_mask.shape == (
            batch_size,
            suffix_tokens.shape[1],
            prefix_tokens.shape[1] + suffix_tokens.shape[1],
        )&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;接着&lt;/span&gt;，计算位置编码，为后缀token计算其在完整序列中的位置，这对于Transformer模型理解序列顺序很重要&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        # 计算后缀token的位置编码
        positions = jnp.sum(prefix_mask, axis=-1)[:, None] + jnp.cumsum(suffix_mask, axis=-1) - 1&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;之后&lt;/span&gt;，模型推理，使用PaliGemma语言模型进行推理，利用缓存的前缀信息（`kv_cache`）提高效率&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;        # 使用KV缓存进行高效的前向传递
        (prefix_out, suffix_out), _ = self.PaliGemma.llm(
            [None, suffix_tokens], mask=full_attn_mask, positions=positions, kv_cache=kv_cache
        )

        # 且确保前缀输出为None（因为使用了KV缓存）
        assert prefix_out is None&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;u&gt;&lt;strong&gt;第四，step函数中做最后的速度预测与动作更新(去噪)&lt;/strong&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;在每一步中，模型预测速度场 `v_t`（从噪声到目标的方向），并通过类欧拉法更新动作表示——使用简单而有效的欧拉方法进行轨迹采样&lt;/p&gt; 
&lt;p&gt;本质就是对去噪，而便是预测的噪声，是时间步长——如上面说过的「&lt;span style=&quot;color:#4da8ee&quot;&gt;&lt;em&gt;时间步长`dt`为负值（因为是从t=1向t=0方向演化），生成初始随机噪声作为起点，且时间上约定：&quot;t=1是噪声，t=0是目标分布&quot;&lt;/em&gt;&lt;/span&gt;」&lt;/p&gt; 
&lt;p&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;具体而言&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt;&lt;li&gt;一方面，提取模型输出并预测速度场`v_t`——&lt;em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;相当于本质是通过PaliGemma模型预测去噪方向 `v_t`&lt;/span&gt;&lt;/em&gt;
&lt;/li&gt;&lt;/ul&gt; 
&lt;pre&gt;&lt;code&gt;        # 预测噪声
        v_t = self.action_out_proj(suffix_out[:, -self.action_horizon :])&lt;/code&gt;&lt;/pre&gt; 
&lt;ul&gt;&lt;li&gt;二方面，使用欧拉法更新动作状态和时间步&lt;/li&gt;&lt;/ul&gt; 
&lt;pre&gt;&lt;code&gt;        # 使用欧拉方法更新状态和时间
        return x_t + dt * v_t, time + dt&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;至于cond函数确定何时停止迭代，通过检查时间是否接近零(当然，要考虑浮点精读可能存在的误差)&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    def cond(carry):
        &quot;&quot;&quot;定义循环终止条件&quot;&quot;&quot;
        x_t, time = carry

        # 考虑浮点误差，当时间接近0时停止
        return time &amp;gt;= -dt / 2&lt;/code&gt;&lt;/pre&gt; 
&lt;h3 id=&quot;1.3%20%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E7%8E%B0%EF%BC%9Amodels%2Fgemma.py&quot; name=&quot;1.3%20%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E7%8E%B0%EF%BC%9Amodels%2Fgemma.py&quot;&gt;1.3 语言模型实现：models/gemma.py&lt;/h3&gt; 
&lt;p&gt;src/openpi/models/gemma.py实现了Gemma语言模型的核心组件，定义了RMSNorm、Embedder、Attention、FeedForward等模块，且提供了不同规模Gemma模型的配置（300M, 2B等）&lt;/p&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;h3 id=&quot;1.4%20%E8%A7%86%E8%A7%89%E6%A8%A1%E5%9E%8B%E5%AE%9E%E7%8E%B0%EF%BC%9Amodels%2Fsiglip.py&quot; name=&quot;1.4%20%E8%A7%86%E8%A7%89%E6%A8%A1%E5%9E%8B%E5%AE%9E%E7%8E%B0%EF%BC%9Amodels%2Fsiglip.py&quot;&gt;1.4 视觉模型实现：models/siglip.py&lt;/h3&gt; 
&lt;p&gt;`siglip.py`: 实现了视觉编码器，基于Vision Transformer (ViT)，定义了位置编码、注意力池化等组件，支持不同大小的模型变体&lt;/p&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;h3&gt;1.5 tokenizer.py: 提供文本tokenization功能&lt;/h3&gt; 
&lt;p&gt;这段代码实现了两个相关但功能不同的tokenizer类：`PaligemmaTokenizer` 和 `FASTTokenizer`&lt;/p&gt; 
&lt;h4&gt;1.5.1 PaligemmaTokenizer 类：专门处理文本prompt&lt;/h4&gt; 
&lt;p&gt;`PaligemmaTokenizer` 是一个相对简单的Tokenizer，专门处理文本prompt&lt;/p&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;u&gt;第一方面&lt;/u&gt;&lt;/span&gt;，在初始化阶段&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`__init__` 方法接收一个 `max_len` 参数（默认为 48）来设定token序列的最大长度 &lt;pre&gt;&lt;code&gt;    # 初始化方法，设置最大token长度，默认为48
    def __init__(self, max_len: int = 48):  
        # 存储最大token长度
        self._max_len = max_len&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;接着，它调用 `download.maybe_download` 函数从 Google Cloud Storage 获取预训练的 PaliGemma 分词模型 &lt;pre&gt;&lt;code&gt;        # 下载PaliGemma分词器模型
        path = download.maybe_download(&quot;gs://big_vision/paligemma_tokenizer.model&quot;, gs={&quot;token&quot;: &quot;anon&quot;})  &lt;/code&gt;&lt;/pre&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;这个下载机制设计得很智能：如果本地缓存中已存在该模型，则直接使用，避免重复下载；否则，会创建一个锁文件确保并发安全，并从 `gs://big_vision/paligemma_tokenizer.model` 下载模型文件。参数 `gs={&quot;token&quot;: &quot;anon&quot;}` 表示使用匿名方式访问 GCS 存储桶&lt;/em&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;下载完成后，代码以二进制读取模式打开文件，并使用 SentencePiece 处理器加载模型 &lt;pre&gt;&lt;code&gt;        # 以二进制读取模式打开下载的模型文件
        with path.open(&quot;rb&quot;) as f:  
            # 初始化SentencePiece处理器
            self._tokenizer = sentencepiece.SentencePieceProcessor(model_proto=f.read())  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;第二方面&lt;/span&gt;&lt;/u&gt;，`tokenize` 方法是处理文本输入的核心，它执行以下步骤：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;文本清理：首先通过 `strip()` 去除首尾空白，然后将下划线替换为空格，并将换行符也替换为空格，确保输入文本格式一致 &lt;pre&gt;&lt;code&gt;    # 定义分词方法，输入为提示文本，返回tokens和mask
    def tokenize(self, prompt: str) -&amp;gt; tuple[np.ndarray, np.ndarray]:  
        # 清理文本：移除首尾空格，将下划线和换行符替换为空格
        cleaned_text = prompt.strip().replace(&quot;_&quot;, &quot; &quot;).replace(&quot;\n&quot;, &quot; &quot;)
        &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;Tokenizer：将清理后的文本送入 SentencePiece 编码器，设置 `add_bos=True` 添加&lt;strong&gt;句子开始token&lt;/strong&gt; &lt;pre&gt;&lt;code&gt;        # 单独将&quot;\n&quot;作为&quot;答案开始&quot;的token
        # 对清理后的文本编码，添加开始标记，并附加换行符的编码
        tokens = self._tokenizer.encode(cleaned_text, add_bos=True) + self._tokenizer.encode(&quot;\n&quot;)  &lt;/code&gt;&lt;/pre&gt; 特别的是，它还单独编码了一个换行符 `\n` 并将其附加到token序列末尾，作为&lt;strong&gt;&quot;答案开始&quot;的特殊token&lt;/strong&gt;。这种设计允许模型明确区分提示和生成内容的边界&lt;/li&gt;
&lt;li&gt;长度处理：根据实际编码后的token序列长度 &lt;pre&gt;&lt;code&gt;        # 获取token列表长度
        tokens_len = len(tokens)  &lt;/code&gt;&lt;/pre&gt; 代码采取两种策略：&lt;br&gt;   如果token数少于 `max_len`，则用 `False` 值填充 `tokens` 序列，同时创建一个掩码 `mask`，其中实际token位置为 `True`(&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;如此，&lt;em&gt;填充&lt;/em&gt;位置自然为 `False`&lt;/em&gt;&lt;/span&gt;) &lt;pre&gt;&lt;code&gt;        # 如果token长度小于最大长度
        if tokens_len &amp;lt; self._max_len:  
            # 创建填充列表，用False填充
            padding = [False] * (self._max_len - tokens_len)  

            # 创建mask列表，真实token位置为True(如此，填充位置自然为False)
            mask = [True] * tokens_len + padding  

            # 对token列表进行填充
            tokens = tokens + padding&lt;/code&gt;&lt;/pre&gt;   如果token数超过 `max_len`，则发出警告并截断序列，掩码全部设为 `True`（因为所有保留的位置都是有效token） &lt;pre&gt;&lt;code&gt;        # 如果token长度大于或等于最大长度
        else:  
            # 如果token长度大于最大长度
            if len(tokens) &amp;gt; self._max_len:  

                # 记录警告日志
                logging.warning(  
                    # 警告token长度超出最大长度，将进行截断
                    f&quot;Token length ({len(tokens)}) exceeds max length ({self._max_len}), truncating. &quot;  
                    # 建议如果频繁发生，增加模型配置中的最大token长度
                    &quot;Consider increasing the `max_token_len` in your model config if this happens frequently.&quot;  
                )

            # 截断token列表，只保留前max_len个
            tokens = tokens[: self._max_len]  
            # 创建全True的mask列表，长度为max_len
            mask = [True] * self._max_len  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;返回结果：最后，方法将token序列和掩码转换为 NumPy 数组并返回，便于后续的模型处理 &lt;pre&gt;&lt;code&gt;        # 将token列表和mask列表转换为numpy数组并返回
        return np.asarray(tokens), np.asarray(mask)  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;1.5.2 FASTTokenizer 类&lt;/h4&gt; 
&lt;p&gt;`FASTTokenizer` 是一个更复杂的Tokenizer，可同时处理文本和动作数据，详见此文《π0开源了且推出自回归版π0-FAST——打造高效Tokenizer：比扩散π0的训练速度快5倍但效果相当(含π0-FAST源码剖析)》&lt;/p&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;首先是初始化过程&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;同样下载 PaliGemma Tokenizer模型 &lt;pre&gt;&lt;code&gt;# 定义FAST分词器类
class FASTTokenizer:  
    # 初始化方法，设置最大长度和FAST分词器路径
    def __init__(self, max_len: int = 256, fast_tokenizer_path: str = &quot;physical-intelligence/fast&quot;):  

        # 存储最大token长度
        self._max_len = max_len  

        # 下载PaliGemma分词器模型
        path = download.maybe_download(&quot;gs://big_vision/paligemma_tokenizer.model&quot;, gs={&quot;token&quot;: &quot;anon&quot;})  

        # 以二进制读取模式打开模型文件
        with path.open(&quot;rb&quot;) as f:  
            self._paligemma_tokenizer = sentencepiece.SentencePieceProcessor(model_proto=f.read()&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;加载专门的 FAST Tokenizer——用于处理动作序列 &lt;pre&gt;&lt;code&gt;        # 实例化FAST分词器
        # 从预训练路径加载FAST处理器
        self._fast_tokenizer = AutoProcessor.from_pretrained(fast_tokenizer_path, trust_remote_code=True)  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;设置 `_fast_skip_tokens = 128` 以跳过 PaliGemma 词汇表末尾的特殊token &lt;pre&gt;&lt;code&gt;        # 跳过PaliGemma词表中的最后128个token，因为它们是特殊token
        self._fast_skip_tokens = 128&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;其次，是Tokenizer流程&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;接收文本提示、状态数组和可选的动作数组 &lt;pre&gt;&lt;code&gt;    # 定义分词方法
    def tokenize(  
        # 输入：提示文本、状态数组和可选的动作数组
        self, prompt: str, state: np.ndarray, actions: np.ndarray | None  
     # 返回四个numpy数组：tokens、token_mask、ar_mask和loss_mask
    ) -&amp;gt; tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: 

        # 清理文本：转小写，移除首尾空格，将下划线替换为空格
        cleaned_text = prompt.lower().strip().replace(&quot;_&quot;, &quot; &quot;)  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;将状态值离散化为 256 个区间（范围 [-1, 1]） &lt;pre&gt;&lt;code&gt;        # 约定：状态被离散化为256个离散区间（假设归一化后的范围：[-1, 1]）
        # 将状态数组离散化为0-255的整数
        discretized_state = np.digitize(state, bins=np.linspace(-1, 1, 256 + 1)[:-1]) - 1  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;创建格式化前缀prefix，包含文本提示和状态信息 &lt;pre&gt;&lt;code&gt;        # 约定：前缀包括提示和状态的字符串表示，后跟&#039;;&#039;
        # 将离散化状态转换为空格分隔的字符串
        state_str = &quot; &quot;.join(map(str, discretized_state))  

        # 构建前缀文本，包含任务和状态信息
        prefix = f&quot;Task: {cleaned_text}, State: {state_str};\n&quot;  

        # 使用PaliGemma分词器编码前缀，添加开始token
        prefix_tokens = self._paligemma_tokenizer.encode(prefix, add_bos=True)  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;如果提供了动作：&lt;br&gt; 使用 FAST Tokenizer对动作进行Tokenizer &lt;pre&gt;&lt;code&gt;        # 如果提供了动作
        if actions is not None:  
            # 使用FAST分词器对动作进行分词，并映射到PaliGemma词表的最后部分

            # 将动作转换为token
            action_tokens = self._fast_tokenizer(actions[None])[0]&lt;/code&gt;&lt;/pre&gt; 通过 `_act_tokens_to_paligemma_tokens` 将这些动作token映射到 PaliGemma 词汇表中 &lt;pre&gt;&lt;code&gt;            # 将FAST token转换为PaliGemma token
            action_tokens_in_pg = self._act_tokens_to_paligemma_tokens(action_tokens)  &lt;/code&gt;&lt;/pre&gt; 创建包含 &quot;Action:&quot; 的后缀，后跟编码的动作和结束符 &quot;|&quot; &lt;pre&gt;&lt;code&gt;            # 约定：后缀包含&#039;Action:&#039;，然后是FAST token，最后是&#039;|&#039;
            # 构建后缀token
            postfix_tokens = (  
                # 编码&quot;Action: &quot;文本
                self._paligemma_tokenizer.encode(&quot;Action: &quot;)  
                 # 添加转换后的动作token
                + action_tokens_in_pg.tolist() 
                # 添加结束分隔符&#039;|&#039;的编码
                + self._paligemma_tokenizer.encode(&quot;|&quot;)  
            )

        # 如果没有提供动作
        else:  
            # 后缀token为空列表
            postfix_tokens = []&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;创建三种掩码：&lt;br&gt;   token_mask：指示哪些位置有实际token&lt;br&gt; &lt;strong&gt;  ar_mask：自回归掩码（前缀为 0，后缀为 1），控制注意力流&lt;/strong&gt;&lt;br&gt;   loss_mask：控制哪些标记在训练期间贡献损失（只在后缀上计算损失） &lt;pre&gt;&lt;code&gt;        # 创建输出token序列和掩码
        # AR掩码在前缀上为0（双向注意力），在后缀上为1（对所有先前token的因果注意力）

        # 合并前缀和后缀token
        tokens = prefix_tokens + postfix_tokens  

        # 创建token掩码，全为True
        token_mask = [True] * len(tokens)  

        # 创建自回归掩码，前缀部分为0，后缀部分为1
        ar_mask = [0] * len(prefix_tokens) + [1] * len(postfix_tokens)  

        # 创建损失掩码，仅在后缀部分计算损失
        loss_mask = [False] * len(prefix_tokens) + [True] * len(postfix_tokens)  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;处理所有token序列和掩码的填充或截断&lt;/li&gt;
&lt;/ol&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;别忘了，上文所说的&lt;/p&gt; 
 &lt;hr&gt; 
 &lt;p id=&quot;2.1.4.2%20%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E6%96%B9%E6%B3%95%EF%BC%9Aembed_prefix(%E5%9B%BE%E5%83%8F%E5%92%8C%E6%96%87%E6%9C%AC%E8%BE%93%E5%85%A5)%E3%80%81embed_suffix(%E7%8A%B6%E6%80%81%E5%92%8C%E5%8A%A8%E4%BD%9C%E4%BF%A1%E6%81%AF)%E2%80%8B%E7%BC%96%E8%BE%91&quot; name=&quot;2.1.4.2%20%E7%89%B9%E5%BE%81%E5%B5%8C%E5%85%A5%E6%96%B9%E6%B3%95%EF%BC%9Aembed_prefix(%E5%9B%BE%E5%83%8F%E5%92%8C%E6%96%87%E6%9C%AC%E8%BE%93%E5%85%A5)%E3%80%81embed_suffix(%E7%8A%B6%E6%80%81%E5%92%8C%E5%8A%A8%E4%BD%9C%E4%BF%A1%E6%81%AF)%E2%80%8B%E7%BC%96%E8%BE%91&quot;&gt;&lt;strong&gt;&lt;span style=&quot;color:null&quot;&gt;1.2.4.2 特征嵌入方法：&lt;/span&gt;embed_prefix(图像和文本输入)、embed_suffix(状态和动作信息)&lt;/strong&gt;&lt;/p&gt; 
  
 &lt;ul&gt;
&lt;li&gt;`embed_prefix`：处理图像和文本输入(&lt;span style=&quot;color:#1c7892&quot;&gt;&lt;em&gt;图像通过SigLip模型编码，文本通过Gemma LLM编码&lt;/em&gt;&lt;/span&gt;)，创建前缀 token，皆为双向注意力，用ar_mask = false表示&lt;/li&gt;
&lt;li&gt;`embed_suffix`：处理机器人状态信息、噪声化的动作信息(&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;em&gt;状态和噪声动作经过线性投影和MLP处理&lt;/em&gt;&lt;/span&gt;)，创建后缀 token&lt;br&gt; 其中&lt;br&gt;   &lt;strong&gt;状态为单个token，和第一个动作token&lt;/strong&gt;均设置为&lt;u&gt;单向注意力，用ar_mask = true表示&lt;/u&gt;&lt;br&gt;   其余动作tokens之间设置为双向注意力，用ar_mask = false表示&lt;/li&gt;
&lt;/ul&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;再其次，是动作提取功能&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;从token序列中提取动作 &lt;pre&gt;&lt;code&gt;    # 定义从token中提取动作的方法
    def extract_actions(self, tokens: np.ndarray, action_horizon: int, action_dim: int) -&amp;gt; np.ndarray:  
        # 解码预测的输出token —— 将token列表解码为文本
        decoded_tokens = self._paligemma_tokenizer.decode(tokens.tolist())  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;定位 &quot;Action:&quot; 后和 &quot;|&quot; 前的部分 &lt;pre&gt;&lt;code&gt;        # 从FAST模型输出中提取动作：如果解码文本中不包含&quot;Action: &quot;
        if &quot;Action: &quot; not in decoded_tokens:  
            # 返回全零动作数组
            return np.zeros((action_horizon, action_dim), dtype=np.float32)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;重新映射token以恢复原始动作空间 &lt;pre&gt;&lt;code&gt;        # 从解码的token中提取动作
        raw_action_tokens = np.array(  
            # 提取&quot;Action: &quot;和&quot;|&quot;之间的内容，并编码为token
            self._paligemma_tokenizer.encode(decoded_tokens.split(&quot;Action: &quot;)[1].split(&quot;|&quot;)[0].strip())
        )

        # 将原始action token转换为PaliGemma token格式
        action_tokens = self._act_tokens_to_paligemma_tokens(raw_action_tokens)  

        # 使用FAST分词器将token解码为动作向量
        return self._fast_tokenizer.decode(  
            [action_tokens.tolist()], time_horizon=action_horizon, action_dim=action_dim
        )[0]&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;u&gt;&lt;strong&gt;&lt;span style=&quot;color:#be191c&quot;&gt;最后是token映射函数&lt;/span&gt;&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    # 定义将FAST token转换为PaliGemma token的方法
    def _act_tokens_to_paligemma_tokens(self, tokens: np.ndarray | list[int]) -&amp;gt; np.ndarray:  

         # 如果输入是列表
        if isinstance(tokens, list): 
            # 转换为numpy数组
            tokens = np.array(tokens)  

        # 将FAST token映射到PaliGemma词表的对应位置
        return self._paligemma_tokenizer.vocab_size() - 1 - self._fast_skip_tokens - tokens&lt;/code&gt;&lt;/pre&gt; 
&lt;ol&gt;
&lt;li&gt;`_act_tokens_to_paligemma_tokens` 方法实现了 FAST 动作token到 PaliGemma 词汇空间的双向映射&lt;/li&gt;
&lt;li&gt;计算公式：`vocab_size - 1 - skip_tokens - token_id`&lt;/li&gt;
&lt;li&gt;这种巧妙的映射让两个不同的Tokenizer系统能够协同工作&lt;/li&gt;
&lt;/ol&gt; 
&lt;h3 id=&quot;1.5%20%E5%85%B6%E4%BB%96%E6%94%AF%E6%8C%81%E6%A8%A1%E5%9D%97%EF%BC%9ALoRA%E3%80%81tokenizer%E3%80%81vit%E7%9A%84%E5%AE%9E%E7%8E%B0&quot; name=&quot;1.5%20%E5%85%B6%E4%BB%96%E6%94%AF%E6%8C%81%E6%A8%A1%E5%9D%97%EF%BC%9ALoRA%E3%80%81tokenizer%E3%80%81vit%E7%9A%84%E5%AE%9E%E7%8E%B0&quot;&gt;1.6 `lora.py` ：实现了LoRA (Low-Rank Adaptation)微调方法&lt;/h3&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;如之前所述，&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;「&lt;/em&gt;&lt;/span&gt;&lt;em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;关于什么是LoRA，详见此文《&lt;/span&gt;&lt;em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;&lt;em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;LLM高效参数微调方法：从Prefix Tuning、Prompt Tuning、P-Tuning V1/V2到LoRA、QLoRA(含对模型量化的解释)&lt;/span&gt;&lt;/em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;》的第4部分&lt;/span&gt;&lt;/em&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;」&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;h4&gt;1.6.1 Einsum类中的setup&lt;/h4&gt; 
&lt;p&gt;`setup` 方法，负责初始化模块所需的所有参数&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;首先，方法通过调用 `self.param` 创建了一个名为 &quot;w&quot; 的参数，这是模块的主要权重矩阵&lt;/li&gt;
&lt;li&gt;接下来，代码使用海象运算符（`:=`）检查是否提供了 `lora_config`。如果存在配置，则进入 LoRA 参数的初始化流程&lt;br&gt; &lt;br&gt; LoRA 的核心思想是将权重更新分解为两个低秩矩阵 A 和 B 的乘积。为此，代码首先创建了原始形状的可变副本 `shape_a` 和 `shape_b`，使用 `list()` 将可能是元组的 `self.shape` 转换为可修改的列表&lt;/li&gt;
&lt;li&gt;随后，`shape_a` 的第二个指定轴（由 `config.axes[1]` 索引）被替换为 `config.rank`&lt;br&gt; 而 `shape_b` 的第一个指定轴（由 `config.axes[0]` 索引）也被替换为相同的 `config.rank`&lt;br&gt; &lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;说白了，就是A矩阵是降维矩阵，故第二个指定轴是rank&lt;br&gt; b是升维矩阵，故b的第一个指定轴是rank&lt;/em&gt;&lt;/span&gt;  &lt;/li&gt;
&lt;li&gt;最后，代码使用 `config.init_fn` 初始化函数(通常是一个小标准差的正态分布)和修改后的形状，创建了两个 LoRA 参数：`self.w_a` 和 `self.w_b`。这些参数分别对应于 LoRA 的 A 和 B 矩阵，它们将在前向传播过程中用于计算 LoRA 更新&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;1.6.2 Einsum类中的__call__&lt;/h4&gt; 
&lt;p&gt;`__call__` 方法实现了支持 LoRA (Low-Rank Adaptation) 技术的前向传播逻辑&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;首先，方法获取并存储输入张量 `x` 的数据类型 (`dtype`)&lt;/li&gt;
&lt;li&gt;接下来，方法使用 `jnp.einsum` 函数计算标准的 Einstein 求和乘积，将输入 `x` 与权重矩阵 `self.w` 相乘。注意权重矩阵会被显式转换为与输入相同的数据类型，这是通过 `self.w.astype(dtype)` 实现的&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;此操作产生的 `result` 变量表示不带 LoRA 修正的基础输出&lt;/em&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;如果模块配置了 LoRA（通过 `self.lora_config` 存在），代码会进入 LoRA 计算分支。使用海象运算符 (`:=`) 既检查了 `lora_config` 的存在性，又将其赋值给局部变量 `config` 以便后续使用  LoRA 计算过程首先调用 `self._make_lora_eqns` 方法，将原始 einsum 方程转换为两个新方程 `eqn_a` 和 `eqn_b`，分别用于与 LoRA 矩阵 A 和 B 的乘法运算&lt;br&gt; &lt;br&gt; 然后，代码执行这两个 einsum 运算：第一个将输入 `x` 与矩阵 A (`self.w_a`) 相乘，结果存储在 `lora` 变量中；第二个将 `lora` 与矩阵 B (`self.w_b`) 相乘，更新 `lora` 变量&lt;br&gt; 同样，为保持数值一致性，LoRA 参数也会被转换为与输入相同的数据类型&lt;br&gt; &lt;br&gt; 最后，将 LoRA 计算结果乘以配置中指定的缩放值 (`config.scaling_value`)——&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;缩放因子通常设置为 `alpha/rank` 或对于 RS-LoRA 为 `alpha/sqrt(rank)`&lt;/em&gt;&lt;/span&gt;，并将其添加到基础输出中，形成最终结果&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;1.6.3 Einsum类中的_make_lora_eqns&lt;/h4&gt; 
&lt;p&gt;_make_lora_eqns负责将标准的 Einstein 求和表达式转换为两个新的表达式，以支持 LoRA 的低秩分解计算。其工作原理基于巧妙的字符串处理，将一个矩阵乘法操作分解为两个连续的矩阵乘法&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;方法首先执行两项重要的验证&lt;br&gt;   第一个验证，它检查输入的方程 `eqn` 中是否已经包含字符 &quot;L&quot;（默认的 LoRA 标签）&lt;br&gt; 如果存在，方法会抛出 `ValueError` 异常，因为 &quot;L&quot; 被保留用作 LoRA 的特殊维度标识符 &lt;pre&gt;&lt;code&gt;    def _make_lora_eqns(self, eqn: str) -&amp;gt; tuple[str, str]:
        if &quot;L&quot; in eqn:
            raise ValueError(f&quot;L already in eqn: {eqn}&quot;)&lt;/code&gt;&lt;/pre&gt;   第二个验证，方法使用正则表达式 `re.match(&quot;(.*),(.*)-&amp;gt;(.*)&quot;, eqn)` 解析输入的 einsum 方程。此正则表达式期望方程遵循标准格式 &quot;&lt;span style=&quot;color:#1a439c&quot;&gt;lhs&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;rhs&lt;/span&gt;-&amp;gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;out&lt;/span&gt;&quot;，其中包含三个捕获组：&lt;span style=&quot;color:#1a439c&quot;&gt;左侧(输入)&lt;/span&gt;、&lt;span style=&quot;color:#1c7331&quot;&gt;右侧(权重)&lt;/span&gt;和&lt;span style=&quot;color:#fe2c24&quot;&gt;输出&lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;如果方程格式不符合此模式，方法会抛出另一个 `ValueError`&lt;/em&gt;&lt;/span&gt; &lt;pre&gt;&lt;code&gt;        if not (m := re.match(&quot;(.*),(.*)-&amp;gt;(.*)&quot;, eqn)):
            raise ValueError(f&quot;Unsupported einsum eqn: {eqn}&quot;)&lt;/code&gt;&lt;/pre&gt; 成功匹配后，方法通过调用 `m.groups()` 提取这三个组件，并将它们分别存储在 `lhs`、`rhs` 和 `out` 变量中 
  &lt;div&gt; 
   &lt;pre&gt;&lt;code&gt;        lhs, rhs, out = m.groups()&lt;/code&gt;&lt;/pre&gt; 
  &lt;/div&gt; &lt;em&gt;例如，对于方程 &quot;&lt;span style=&quot;color:#1a439c&quot;&gt;bd&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;dh&lt;/span&gt;-&amp;gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;bh&lt;/span&gt;&quot;，这些变量将分别包含 &quot;bd&quot;、&quot;dh&quot; 和 &quot;bh&quot;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;接下来是方法的核心部分&lt;br&gt; &lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;首先&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;，根据 `self.lora_config.axes` 指定的索引，从 `rhs` 字符串中提取两个关键轴标签 `a_label` 和 `b_label` &lt;pre&gt;&lt;code&gt;        assert self.lora_config is not None
        a_label, b_label = (rhs[x] for x in self.lora_config.axes)
        label = self.lora_config.label&lt;/code&gt;&lt;/pre&gt; &lt;em&gt;例如，如果 `&lt;span style=&quot;color:#1c7331&quot;&gt;rhs&lt;/span&gt;` 是 &quot;&lt;span style=&quot;color:#1c7331&quot;&gt;dh&lt;/span&gt;&quot; 且 `axes` 为 (-2, -1)——代表最后两个轴，则&lt;br&gt; `&lt;strong&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;a_label&lt;/span&gt;&lt;/strong&gt;` 为 &quot;&lt;span style=&quot;color:#1c7331&quot;&gt;d&lt;/span&gt;&quot;&lt;br&gt; `&lt;strong&gt;b_label&lt;/strong&gt;` 为 &quot;&lt;strong&gt;&lt;span style=&quot;color:#1c7331&quot;&gt;h&lt;/span&gt;&lt;/strong&gt;&quot;&lt;/em&gt;&lt;br&gt; &lt;br&gt; &lt;strong&gt;&lt;u&gt;&lt;span style=&quot;color:#be191c&quot;&gt;其次&lt;/span&gt;&lt;/u&gt;&lt;/strong&gt;，进行两步字符串替换，创建两个新的 einsum 方程&lt;br&gt;   &lt;strong&gt;第一步&lt;/strong&gt;，它将 `&lt;span style=&quot;color:#1c7331&quot;&gt;rhs&lt;/span&gt;` 和 `&lt;span style=&quot;color:#fe2c24&quot;&gt;out&lt;/span&gt;` 中的 `&lt;strong&gt;b_label&lt;/strong&gt;` 替换为 LoRA 标签(存储在 `label` 变量中，默认为 &quot;L&quot;)。这产生了 `&lt;span style=&quot;color:#1c7331&quot;&gt;a_rhs&lt;/span&gt;` 和 `&lt;span style=&quot;color:#fe2c24&quot;&gt;a_out&lt;/span&gt;`，用于构造第一个方程 `eqn_a`&lt;br&gt; &lt;br&gt; &lt;em&gt;例如，对于前面 &quot;&lt;span style=&quot;color:#1a439c&quot;&gt;lhs&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;rhs&lt;/span&gt;-&amp;gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;out&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;所对应&lt;/span&gt;的例子&quot;&lt;span style=&quot;color:#1a439c&quot;&gt;bd&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;d&lt;u&gt;h&lt;/u&gt;&lt;/span&gt;-&amp;gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;b&lt;u&gt;h&lt;/u&gt;&lt;/span&gt;&quot;，&lt;/em&gt;&lt;em&gt;`&lt;span style=&quot;color:#1c7331&quot;&gt;a_rhs&lt;/span&gt;`-&lt;span style=&quot;color:#1c7331&quot;&gt;d&lt;u&gt;h&lt;/u&gt;&lt;/span&gt; 会变成 &quot;&lt;span style=&quot;color:#1c7331&quot;&gt;d&lt;u&gt;L&lt;/u&gt;&lt;/span&gt;&quot;，`&lt;span style=&quot;color:#fe2c24&quot;&gt;a_out&lt;/span&gt;`-&lt;span style=&quot;color:#fe2c24&quot;&gt;b&lt;u&gt;h&lt;/u&gt;&lt;/span&gt; 会变成 &quot;&lt;span style=&quot;color:#fe2c24&quot;&gt;b&lt;u&gt;L&lt;/u&gt;&lt;/span&gt;&quot;&lt;/em&gt; &lt;pre&gt;&lt;code&gt;        a_rhs = rhs.replace(b_label, label)
        a_out = out.replace(b_label, label)&lt;/code&gt;&lt;/pre&gt; 生成的 `eqn_a` 为 &quot;&lt;span style=&quot;color:#1a439c&quot;&gt;bd&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;d&lt;u&gt;L&lt;/u&gt;&lt;/span&gt;-&amp;gt;&lt;strong&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;b&lt;u&gt;L&lt;/u&gt;&lt;/span&gt;&lt;/strong&gt;&quot;，表示将输入&lt;span style=&quot;color:#1a439c&quot;&gt;bd &lt;/span&gt;与 LoRA 矩阵 A &lt;span style=&quot;color:#1c7331&quot;&gt;d&lt;u&gt;L&lt;/u&gt;&lt;/span&gt;相乘，得到此&lt;strong&gt;第一步的结果&lt;span style=&quot;color:#fe2c24&quot;&gt;b&lt;u&gt;L&lt;/u&gt;&lt;/span&gt;&lt;/strong&gt; &lt;pre&gt;&lt;code&gt;        eqn_a = f&quot;{lhs},{a_rhs}-&amp;gt;{a_out}&quot;&lt;/code&gt;&lt;/pre&gt;   &lt;strong&gt;第二步&lt;/strong&gt;，方法创建 `b_rhs`，通过将 `rhs` 中的 `&lt;strong&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;a_label&lt;/span&gt;&lt;/strong&gt;` 替换为 LoRA 标签&lt;br&gt; 使用&lt;em&gt;前面 &quot;&lt;span style=&quot;color:#1a439c&quot;&gt;lhs&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;rhs&lt;/span&gt;-&amp;gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;out&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;所对应&lt;/span&gt;的例子&quot;&lt;span style=&quot;color:#1a439c&quot;&gt;bd&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;u&gt;d&lt;/u&gt;h&lt;/span&gt;-&amp;gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;bh&lt;/span&gt;&quot;&lt;/em&gt;&lt;br&gt; `&lt;span style=&quot;color:#1c7331&quot;&gt;b_rhs-&lt;/span&gt;&lt;em&gt;&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;u&gt;d&lt;/u&gt;h&lt;/span&gt;&lt;/em&gt;` 将变为 &quot;&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;u&gt;L&lt;/u&gt;h&lt;/span&gt;&quot; &lt;pre&gt;&lt;code&gt;        b_rhs = rhs.replace(a_label, label)&lt;/code&gt;&lt;/pre&gt; 然后构造第二个方程 `eqn_b`，形式为 &quot;&lt;strong&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;bL/&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;color:#1a439c&quot;&gt;bL&lt;/span&gt;,&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;u&gt;L&lt;/u&gt;h&lt;/span&gt;-&amp;gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;bh&lt;/span&gt;&quot;&lt;br&gt; 为何这里的输入是&lt;strong&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;bL/&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;color:#1a439c&quot;&gt;bL&lt;/span&gt;呢，因为其表示的就是&lt;strong&gt;将第一步的结果&lt;span style=&quot;color:#fe2c24&quot;&gt;bL&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#1a439c&quot;&gt;bL&lt;/span&gt;与 LoRA 矩阵 B &lt;span style=&quot;color:#1c7331&quot;&gt;&lt;u&gt;L&lt;/u&gt;h &lt;/span&gt;相乘 &lt;pre&gt;&lt;code&gt;        eqn_b = f&quot;{a_out},{b_rhs}-&amp;gt;{out}&quot;&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;最后，方法返回这两个新创建的 einsum 方程作为元组 &lt;pre&gt;&lt;code&gt;        return eqn_a, eqn_b&lt;/code&gt;&lt;/pre&gt; 这些方程将被用于在前向传播过程中计算 LoRA 的低秩更新&lt;/li&gt;
&lt;/ol&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;总的来说，上面的整个过程 还是比较绕的，为方便大家一目了然的快速理解，我特意花了10分钟画了个图示——&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;而我一个人多花10分钟，可以让数千人、数万人在理解上 少花10分钟，这价值非常大&lt;/em&gt;&lt;/span&gt;，会更清晰&lt;/p&gt; 
  
&lt;/blockquote&gt; 
&lt;h4&gt;1.6.4 FeedForward类中的setup、__call__、_dot&lt;/h4&gt; 
&lt;h3&gt;1.7 `vit.py`: Vision Transformer实现&lt;/h3&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E7%AC%AC%E5%9B%9B%E9%83%A8%E5%88%86%20%E7%AD%96%E7%95%A5%E9%80%82%E9%85%8D%E6%8E%A5%E5%8F%A3%EF%BC%9Asrc%E4%B8%8Bpolicy%E7%9A%84%E5%85%A8%E9%9D%A2%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%A3%E8%AF%BB&quot; name=&quot;%E7%AC%AC%E5%9B%9B%E9%83%A8%E5%88%86%20%E7%AD%96%E7%95%A5%E9%80%82%E9%85%8D%E6%8E%A5%E5%8F%A3%EF%BC%9Asrc%E4%B8%8Bpolicy%E7%9A%84%E5%85%A8%E9%9D%A2%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%A3%E8%AF%BB&quot;&gt;第二部分 策略适配接口：src下policy的全面分析与解读&lt;/h2&gt; 
&lt;p&gt;src/openpi/policies目录包含以下文件：&lt;/p&gt; 
&lt;p&gt;BasePolicy (policy.py)&lt;br&gt; ├── Policy&lt;br&gt; │   ├── BaseModel&lt;br&gt; │   └── transforms.py&lt;br&gt; ├── AlohaPolicy (aloha_policy.py)&lt;br&gt; ├── DroidPolicy (droid_policy.py)&lt;br&gt; └── LiberoPolicy (libero_policy.py)&lt;/p&gt; 
&lt;ol&gt;&lt;/ol&gt; 
&lt;p&gt;此外，每个特定机器人都有自己的策略文件，如&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;aloha_policy.py&lt;/li&gt;
&lt;li&gt;droid_policy.py&lt;/li&gt;
&lt;li&gt;libero_policy.py&lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;这些文件定义了特定于机器人的输入和输出转换函数，处理数据格式、规范化和特定的转换需求&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;比如每种机器人（ALOHA、DROID、LIBERO）的策略文件定义了特定的输入/输出转换类&lt;/li&gt;
&lt;li&gt;这些转换类作为 `transforms` 参数传递给 `Policy` 构造函数，例如，`AlohaInputs` 处理 ALOHA 机器人特有的状态和图像格式，`AlohaOutputs` 处理对应的输出转换&lt;/li&gt;
&lt;/ol&gt; 
&lt;h3 id=&quot;4.1%C2%A0policy.py%EF%BC%9A%E5%AE%9E%E7%8E%B0%E4%BA%86Policy%E7%B1%BB%E5%92%8C%20PolicyRecorder%E7%B1%BB&quot; name=&quot;4.1%C2%A0policy.py%EF%BC%9A%E5%AE%9E%E7%8E%B0%E4%BA%86Policy%E7%B1%BB%E5%92%8C%20PolicyRecorder%E7%B1%BB&quot;&gt;2.1 policy.py：实现了Policy类和 PolicyRecorder类&lt;/h3&gt; 
&lt;h4 id=&quot;4.1.1%20Policy%20%E7%B1%BB&quot; name=&quot;4.1.1%20Policy%20%E7%B1%BB&quot;&gt;2.1.1 Policy 类&lt;/h4&gt; 
&lt;p&gt;policy.py 定义了基本的 `Policy` 类和 `PolicyRecorder` 类，它们继承自`openpi_client.base_policy.BasePolicy`&lt;/p&gt; 
&lt;p&gt;首先，做一系列初始化&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;class Policy(BasePolicy):  # 定义Policy类，继承自BasePolicy
    def __init__(
        self,
        model: _model.BaseModel,  # 模型参数，必须是BaseModel的实例
        *,  # 之后的所有参数必须使用关键字传递
        rng: at.KeyArrayLike | None = None,  # 随机数生成器，可选

        # 输入转换函数序列，默认为空
        transforms: Sequence[_transforms.DataTransformFn] = (),  

        # 输出转换函数序列，默认为空
        output_transforms: Sequence[_transforms.DataTransformFn] = (),  

        # 传递给sample_actions的额外参数，可选
        sample_kwargs: dict[str, Any] | None = None,  

        metadata: dict[str, Any] | None = None,  # 元数据字典，可选
    ):

        # 使用JIT编译model的sample_actions方法提高性能
        self._sample_actions = nnx_utils.module_jit(model.sample_actions)  

        # 组合所有输入转换函数为一个函数
        self._input_transform = _transforms.***pose(transforms)  

        # 组合所有输出转换函数为一个函数
        self._output_transform = _transforms.***pose(output_transforms)  
        self._rng = rng or jax.random.key(0)       # 设置随机数生成器，如果未提供则创建一个新的
        self._sample_kwargs = sample_kwargs or {}  # 存储采样参数，如果未提供则使用空字典
        self._metadata = metadata or {}            # 存储元数据，如果未提供则使用空字典&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其次，对于infer 方法——在策略内部流程上&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;复制输入观察数据 &lt;pre&gt;&lt;code&gt;    def infer(self, obs: dict) -&amp;gt; dict:  # type: ignore[misc]  # 推理方法，接收观察字典，返回动作字典
        # 复制输入，因为转换可能会修改输入
        inputs = jax.tree.map(lambda x: x, obs)  # 使用JAX树映射创建输入的深拷贝&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;应用输入转换&lt;br&gt; Policy.infer` 方法首先应用输入转换：self._input_transform，将客户端提供的观察转换为模型所需的格式 &lt;pre&gt;&lt;code&gt;        inputs = self._input_transform(inputs)  # 应用输入转换函数处理输入数据&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;将数据转换为批处理格式并转为 JAX 数组 &lt;pre&gt;&lt;code&gt;        # 将输入转换为批处理格式并转为jax数组
        inputs = jax.tree.map(lambda x: jnp.asarray(x)[np.newaxis, ...], inputs)  # 添加批次维度并转为JAX数组&lt;/code&gt;&lt;/pre&gt; 生成新的随机数键 &lt;pre&gt;&lt;code&gt;        self._rng, sample_rng = jax.random.split(self._rng)  # 分割随机数键以保持随机性&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;模型推理&lt;br&gt; 调用模型的 `sample_actions` 方法「&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;该方法的实现，详见上文的&lt;strong&gt;1.2.4.4 推理函数 `sample_actions`：基于扩散模型逆向采样，生成机器人动作序列&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;」进行推理，即获取动作预测 &lt;pre&gt;&lt;code&gt;        outputs = {
            &quot;state&quot;: inputs[&quot;state&quot;],  # 保留状态信息
            &quot;actions&quot;: self._sample_actions(sample_rng, _model.Observation.from_dict(inputs), **self._sample_kwargs),  # 使用模型生成动作
        }&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;解除批处理并转换为 NumPy 数组 &lt;pre&gt;&lt;code&gt;        # 移除批次维度并转换为NumPy数组
        outputs = jax.tree.map(lambda x: np.asarray(x[0, ...]), outputs)  # 取第一个样本并转为NumPy数组&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;输出转换&lt;br&gt; 最后应用输出转换 (`self._output_transform`)，将模型输出转换为客户端期望的格式 &lt;pre&gt;&lt;code&gt;        return self._output_transform(outputs)  # 应用输出转换并返回结果&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;4.1.2%20%60PolicyRecorder%60&quot; name=&quot;4.1.2%20%60PolicyRecorder%60&quot;&gt;2.1.2 `PolicyRecorder`&lt;/h4&gt; 
&lt;p&gt;PolicyRecorder是一个装饰器类，它包装了一个基础策略，并在执行策略的同时将所有的输入和输出保存到磁盘，用于记录策略的行为&lt;/p&gt; 
&lt;p&gt;对于初始化函数：`policy`，涉及被包装的基础策略、record_dir`：保存记录的目录路径&lt;/p&gt; 
&lt;p&gt;对于infer 方法&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;调用被包装策略的 `infer` 方法获取结果&lt;/li&gt;
&lt;li&gt;将输入和输出数据组织为字典&lt;/li&gt;
&lt;li&gt;使用 Flax 的 `flatten_dict` 函数将嵌套字典展平&lt;/li&gt;
&lt;li&gt;构建输出文件路径&lt;/li&gt;
&lt;li&gt;将数据保存为 NumPy 数组文件&lt;/li&gt;
&lt;li&gt;返回策略结果&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;h3 id=&quot;4.2%20policy_config.py&quot; name=&quot;4.2%20policy_config.py&quot;&gt;2.2 policy_config.py&lt;/h3&gt; 
&lt;p&gt;policy_config.py 定义了 `PolicyConfig` 类和 `create_trained_policy` 函数&lt;br&gt; `create_trained_policy` 函数用于从训练好的检查点创建策略实例，加载模型参数、归一化统计数据，并配置转换函数&lt;/p&gt; 
&lt;p&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;相当于客户端代码会实例化一个 `Policy` 对象，通常是通过 `create_trained_policy` 函数，客户端通过调用 `policy.infer(obs)` 方法获取策略输出&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h4 id=&quot;4.2.1%20PolicyConfig%20%E6%95%B0%E6%8D%AE%E7%B1%BB&quot; name=&quot;4.2.1%20PolicyConfig%20%E6%95%B0%E6%8D%AE%E7%B1%BB&quot;&gt;2.2.1 PolicyConfig 数据类&lt;/h4&gt; 
&lt;p&gt;`PolicyConfig` 是一个使用 `@dataclasses.dataclass` 装饰的数据类，用于存储创建策略所需的所有配置信息：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt; # 定义策略配置类
class PolicyConfig:     
    model: _model.BaseModel      # 模型实例，必须是BaseModel类型
    norm_stats: dict[str, transforms.NormStats]        # 归一化统计信息，键是特征名称，值是归一化统计数据

    input_layers: Sequence[transforms.DataTransformFn]      # 输入数据转换函数序列
    output_layers: Sequence[transforms.DataTransformFn]     # 输出数据转换函数序列

    model_type: _model.ModelType = _model.ModelType.PI0     # 模型类型，默认为PI0
    default_prompt: str | None = None                  # 默认提示文本，可选
    sample_kwargs: dict[str, Any] | None = None        # 采样参数字典，可选&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;这个类主要是作为配置容器，将所有策略创建时需要的参数组织在一起&lt;/p&gt; 
&lt;h4 id=&quot;4.2.2%20create_trained_policy%20%E5%87%BD%E6%95%B0&quot; name=&quot;4.2.2%20create_trained_policy%20%E5%87%BD%E6%95%B0&quot;&gt;2.2.2 create_trained_policy 函数&lt;/h4&gt; 
&lt;p&gt;`create_trained_policy` 函数是从训练好的检查点创建可用策略的工厂函数&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;def create_trained_policy(
    train_config: _config.TrainConfig,       # 训练配置对象，包含训练时的所有参数设置
    checkpoint_dir: pathlib.Path | str,      # 检查点目录路径，可以是Path对象或字符串
    *,  # 强制后续参数使用关键字传递
    repack_transforms: transforms.Group | None = None,  # 可选的重新打包转换组
    sample_kwargs: dict[str, Any] | None = None,        # 采样参数，可选
    default_prompt: str | None = None,                  # 默认提示文本，可选
    norm_stats: dict[str, transforms.NormStats] | None = None,  # 归一化统计信息，可选
) -&amp;gt; _policy.Policy:                         # 返回类型是Policy对象&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;函数的核心流程是：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;处理输入参数，确保 `repack_transforms` 不为空&lt;br&gt; 且检查并可能下载检查点目录 &lt;pre&gt;&lt;code&gt;    repack_transforms = repack_transforms or transforms.Group()      # 确保repack_transforms不为空，如果未提供则创建空Group
    checkpoint_dir = download.maybe_download(str(checkpoint_dir))    # 检查并可能下载检查点目录&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;使用 `train_config` 加载模型参数 &lt;pre&gt;&lt;code&gt;    logging.info(&quot;Loading model...&quot;)  # 记录日志，表示正在加载模型

    # 加载模型参数并创建模型实例，使用bfloat16数据类型
    model = train_config.model.load(_model.restore_params(checkpoint_dir / &quot;params&quot;, dtype=jnp.bfloat16))  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;创建数据配置 &lt;pre&gt;&lt;code&gt;    data_config = train_config.data.create(train_config.assets_dirs, train_config.model)  # 创建数据配置
    if norm_stats is None:  # 如果未提供归一化统计信息
        # 我们从检查点而非配置资源目录加载归一化统计信息，以确保策略使用与原始训练过程相同的归一化统计信息&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;如果未提供 `norm_stats`，从检查点加载归一化统计信息 &lt;pre&gt;&lt;code&gt;        if data_config.asset_id is None:  # 如果数据配置中没有asset_id
            raise ValueError(&quot;Asset id is required to load norm stats.&quot;)  # 抛出异常，需要asset_id来加载归一化统计信息
        norm_stats = _checkpoints.load_norm_stats(checkpoint_dir / &quot;assets&quot;, data_config.asset_id)  # 从检查点加载归一化统计信息&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;构建并返回 `Policy` 实例，将所有转换函数组织为有序的处理流程： &lt;pre&gt;&lt;code&gt;    return _policy.Policy(  # 创建并返回Policy实例
        model,  # 传入模型&lt;/code&gt;&lt;/pre&gt; 输入处理：重新打包转换 → 注入默认提示 → 数据转换 → 归一化 → 模型特定转换 &lt;pre&gt;&lt;code&gt;        transforms=[  # 输入转换函数序列
            *repack_transforms.inputs,          # 展开重打包转换的输入部分
            transforms.InjectDefaultPrompt(default_prompt),  # 注入默认提示
            *data_config.data_transforms.inputs,   # 展开数据转换的输入部分
            transforms.Normalize(norm_stats, use_quantiles=data_config.use_quantile_norm),      # 添加归一化转换
            *data_config.model_transforms.inputs,  # 展开模型特定转换的输入部分
        ],&lt;/code&gt;&lt;/pre&gt; 输出处理：模型特定转换 → 反归一化 → 数据转换 → 重新打包转换 &lt;pre&gt;&lt;code&gt;        output_transforms=[  # 输出转换函数序列
            *data_config.model_transforms.outputs,     # 展开模型特定转换的输出部分
            transforms.Unnormalize(norm_stats, use_quantiles=data_config.use_quantile_norm),          # 添加反归一化转换
            *data_config.data_transforms.outputs,      # 展开数据转换的输出部分
            *repack_transforms.outputs,           # 展开重打包转换的输出部分
        ],
        sample_kwargs=sample_kwargs,              # 设置采样参数
        metadata=train_config.policy_metadata,    # 设置策略元数据
    )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;`create_trained_policy` 函数是框架中连接训练过的模型与实际部署使用的关键桥梁，它通过组合各种转换函数，创建出可直接用于推理的 `Policy` 实例&lt;/p&gt; 
&lt;h3 id=&quot;4.3%20policies%2Faloha_policy.py&quot; name=&quot;4.3%20policies%2Faloha_policy.py&quot;&gt;2.3 policies/aloha_policy.py&lt;/h3&gt; 
&lt;p&gt;这段代码实现了一个用于 Aloha 策略的输入输出处理和数据转换的模块&lt;/p&gt; 
&lt;h4 id=&quot;4.3.1%C2%A0make_aloha_example%EF%BC%9A%E8%BE%93%E5%85%A5%E7%A4%BA%E4%BE%8B%E2%80%94%E2%80%94%E7%8A%B6%E6%80%81%E5%90%91%E9%87%8F%E3%80%81%E5%9B%BE%E5%83%8F%E6%95%B0%E6%8D%AE%E3%80%81%E6%96%87%E6%9C%ACprompt&quot; name=&quot;4.3.1%C2%A0make_aloha_example%EF%BC%9A%E8%BE%93%E5%85%A5%E7%A4%BA%E4%BE%8B%E2%80%94%E2%80%94%E7%8A%B6%E6%80%81%E5%90%91%E9%87%8F%E3%80%81%E5%9B%BE%E5%83%8F%E6%95%B0%E6%8D%AE%E3%80%81%E6%96%87%E6%9C%ACprompt&quot;&gt;2.3.1 make_aloha_example：输入示例——状态向量、图像数据、文本prompt&lt;/h4&gt; 
&lt;p&gt;首先，`make_aloha_example` 函数创建了一个随机的输入示例，包括一个14维的状态向量和四个摄像头的图像数据（高、低、左腕、右腕视角），以及一个文本提示信息&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;# 定义一个函数，创建Aloha策略的随机输入示例
def make_aloha_example() -&amp;gt; dict:  
    # 返回一个字典，包含状态、图像和提示信息
    return {  
        # 创建一个14维的状态向量，所有值为1
        &quot;state&quot;: np.ones((14,)),  

        # 创建一个包含四个摄像头图像的字典
        &quot;images&quot;: {  
            # 高位摄像头图像
            &quot;cam_high&quot;: np.random.randint(256, size=(3, 224, 224), dtype=np.uint8),  

            # 低位摄像头图像
            &quot;cam_low&quot;: np.random.randint(256, size=(3, 224, 224), dtype=np.uint8), 

            # 左手腕摄像头图像 
            &quot;cam_left_wrist&quot;: np.random.randint(256, size=(3, 224, 224), dtype=np.uint8),  

            # 右手腕摄像头图像
            &quot;cam_right_wrist&quot;: np.random.randint(256, size=(3, 224, 224), dtype=np.uint8),  
        },
        &quot;prompt&quot;: &quot;do something&quot;, 
    }&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;这些数据将用于测试和验证 Aloha 策略的输入处理&lt;/p&gt; 
&lt;blockquote&gt;
 可能有的同学对上面的4个摄像头有疑问，简单，详见此文《
 一文通透动作分块算法ACT：斯坦福ALOHA团队推出的动作序列预测算法(Action Chunking with Transformers)》的「1.2 硬件套装：ALOHA——低成本的开源硬件系统，用于手动远程操作」 
 &lt;hr&gt; 
 &lt;p id=&quot;t-page2-s1-c0-b4-p0&quot;&gt;如下图所示&lt;/p&gt; 
  
 &lt;ul&gt;&lt;li&gt;左侧为前、顶部和两个手腕摄像机的视角(&lt;em&gt;这4个相机的视角分别用&lt;span style=&quot;color:#4da8ee&quot;&gt;从当前往后的蓝线&lt;/span&gt;、&lt;span style=&quot;color:#98c091&quot;&gt;从顶向下的绿线&lt;/span&gt;、&lt;span style=&quot;color:#ed7976&quot;&gt;从左往右的红线&lt;/span&gt;、&lt;/em&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;&lt;em&gt;从右往左的红线&lt;/em&gt;&lt;em&gt;表示&lt;/em&gt;&lt;/span&gt;)，以及ALOHA双手工作空间的示意图&lt;br&gt; &lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;具体而言，总计4个Logitech C922x网络摄像头，每个流输出480×640 RGB图像&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;  其中两个网络摄像头安装在跟随机器人手腕上，以提供夹具的近距离视角(&lt;em&gt;allowing for a close-up view of the grippers&lt;/em&gt;)&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;  剩下的两个相机分别安装在桌面的前方(front camera)和桌子上方的顶部位置(top camera)，遥控操作和数据记录均以50Hz频率进行&lt;/span&gt;
&lt;/li&gt;&lt;/ul&gt; 
&lt;/blockquote&gt; 
&lt;h4 id=&quot;4.3.2%C2%A0AlohaInputs%EF%BC%9A%E5%AE%9A%E4%B9%89Aloha%20%E7%AD%96%E7%95%A5%E7%9A%84%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&quot; name=&quot;4.3.2%C2%A0AlohaInputs%EF%BC%9A%E5%AE%9A%E4%B9%89Aloha%20%E7%AD%96%E7%95%A5%E7%9A%84%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&quot;&gt;2.3.2 AlohaInputs：定义Aloha 策略的输入数据结构&lt;/h4&gt; 
&lt;p&gt;接下来，`AlohaInputs` 类定义了 Aloha 策略的输入数据结构&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;class AlohaInputs(transforms.DataTransformFn):  # 定义AlohaInputs类，继承自transforms.DataTransformFn
    &quot;&quot;&quot;Inputs for the Aloha policy.
    # 预期输入格式
    # 图像字典，键是名称，值是形状为[channel, height, width]的图像
    - images: dict[name, img]

     # 状态向量，长度为14
    - state: [14] 

    # 动作矩阵，形状为[action_horizon, 14]
    - actions: [action_horizon, 14]  
    &quot;&quot;&quot;

    # 模型的动作维度，将用于填充状态和动作
    action_dim: int  # 动作维度

    # 如果为True，将关节和夹持器值从标准Aloha空间转换为pi内部运行时使用的空间
    # pi内部运行时使用的空间用于训练基础模型
    # 是否适配pi内部运行时，默认为True
    adapt_to_pi: bool = True  

    # 预期的摄像头名称，所有输入摄像头必须在此集合中。缺失的摄像头将用黑色图像替代
    # 缺失的摄像头将用黑色图像替代，对应的`image_mask`将设置为False
    # 预期的摄像头名称集合
    EXPECTED_CAMERAS: ClassVar[tuple[str, ...]] = (&quot;cam_high&quot;, &quot;cam_low&quot;, &quot;cam_left_wrist&quot;, &quot;cam_right_wrist&quot;)  &lt;/code&gt;&lt;/pre&gt; 
&lt;ol&gt;
&lt;li&gt;这个类使用 `dataclasses.dataclass` 装饰器来简化类的定义，并确保实例是不可变的（`frozen=True`）&lt;/li&gt;
&lt;li&gt;类中定义了输入数据的预期格式，包括图像、状态和动作数据&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;__call__&lt;/span&gt;方法，实现了对Aloha策略输入数据的标准化处理。该方法将&lt;strong&gt;原始输入数据转换为模型可接受的格式&lt;/strong&gt;，包括多项关键处理步骤，比如进行必要的解码和填充操作，并检查图像数据是否包含预期的摄像头视角&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;首先，方法通过调用`_decode_aloha`函数对输入数据进行初步解码，根据`adapt_to_pi`参数决定是否将数据适配到π内部运行时环境 &lt;pre&gt;&lt;code&gt;    # 定义__call__方法，处理输入数据
    def __call__(self, data: dict) -&amp;gt; dict:  

        # 解码Aloha数据，根据adapt_to_pi参数进行适配
        data = _decode_aloha(data, adapt_to_pi=self.adapt_to_pi)  &lt;/code&gt;&lt;/pre&gt; 这一步主要处理状态向量以及将图像格式从`[channel, height, width]`转换为`[height, width, channel]`&lt;/li&gt;
&lt;li&gt;接着，方法将14维的状态向量使用零填充扩展到模型所需的动作维度(`action_dim`) &lt;pre&gt;&lt;code&gt;        # 获取状态数据，将其从14维填充到模型的动作维度
        # 使用transforms.pad_to_dim函数填充状态数据
        state = transforms.pad_to_dim(data[&quot;state&quot;], self.action_dim)  &lt;/code&gt;&lt;/pre&gt; 随后，进行输入图像的验证：检查输入图像的键集合是否超出了预期的摄像头列表范围，若发现未知摄像头视角则抛出`ValueError` &lt;pre&gt;&lt;code&gt;        # 获取输入图像数据
        in_images = data[&quot;images&quot;]  

        # 检查输入图像是否包含所有预期的摄像头
        if set(in_images) - set(self.EXPECTED_CAMERAS):  
            # 如果缺少预期的摄像头，抛出异常
            raise ValueError(f&quot;Expected images to contain {self.EXPECTED_CAMERAS}, got {tuple(in_images)}&quot;)  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;在构建输出字典时，方法首先假定&quot;cam_high&quot;（高视角摄像头）图像必定存在 &lt;pre&gt;&lt;code&gt;        # 假设基础图像总是存在，获取高位摄像头图像
        base_image = in_images[&quot;cam_high&quot;]  &lt;/code&gt;&lt;/pre&gt; 并将其作为基础图像（`base_0_rgb`） &lt;pre&gt;&lt;code&gt;        # 创建图像字典
        images = {  
            # 基础图像
            &quot;base_0_rgb&quot;: base_image,  
        }&lt;/code&gt;&lt;/pre&gt; 同时创建了相应的图像掩码字典，标记该图像为有效 &lt;pre&gt;&lt;code&gt;        # 创建图像掩码字典
        image_masks = {  
            # 基础图像掩码为True
            &quot;base_0_rgb&quot;: np.True_,  
        }
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;对于其他摄像头视角（左腕和右腕），方法使用映射关系字典进行处理： &lt;pre&gt;&lt;code&gt;        # 添加额外的图像
        # 额外图像名称映射
        extra_image_names = {  
            # 左手腕图像
            &quot;left_wrist_0_rgb&quot;: &quot;cam_left_wrist&quot;,  

            # 右手腕图像
            &quot;right_wrist_0_rgb&quot;: &quot;cam_right_wrist&quot;,  
        }&lt;/code&gt;&lt;/pre&gt; 如果相应的源图像存在，则将其添加到输出图像字典并标记为有效； &lt;pre&gt;&lt;code&gt;        # 遍历额外图像名称映射
        for dest, source in extra_image_names.items():  

            # 如果输入图像中包含该图像
            if source in in_images:  
                # 添加到图像字典
                images[dest] = in_images[source]  

                # 设置图像掩码为True
                image_masks[dest] = np.True_&lt;/code&gt;&lt;/pre&gt; 若不存在，则创建一个与基础图像相同大小的全零图像（黑图），并标记为无效 &lt;pre&gt;&lt;code&gt;            # 如果输入图像中不包含该图像
            else:  
                # 用黑色图像替代
                images[dest] = np.zeros_like(base_image)  

                # 设置图像掩码为False
                image_masks[dest] = np.False_&lt;/code&gt;&lt;/pre&gt; 这种处理方式确保了模型在缺失某些视角图像时仍能正常工作 &lt;pre&gt;&lt;code&gt;        # 创建输入字典
        inputs = {  
            &quot;image&quot;: images,              # 图像数据
            &quot;image_mask&quot;: image_masks,    # 图像掩码
            &quot;state&quot;: state,               # 状态数据
        }&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;方法还会处理训练时特有的数据，如动作序列&lt;br&gt; 若输入数据包含&quot;actions&quot;字段，则将其转换为NumPy数组，应用`_encode_actions_inv`进行编码转换，并使用零填充扩展到模型动作维度 &lt;pre&gt;&lt;code&gt;        # 动作数据仅在训练期间可用
        # 如果输入数据中包含动作数据
        if &quot;actions&quot; in data:  
             # 将动作数据转换为NumPy数组
            actions = np.asarray(data[&quot;actions&quot;]) 

            # 编码动作数据，根据adapt_to_pi参数进行适配
            actions = _encode_actions_inv(actions, adapt_to_pi=self.adapt_to_pi)  

            # 填充动作数据到模型的动作维度
            inputs[&quot;actions&quot;] = transforms.pad_to_dim(actions, self.action_dim)  &lt;/code&gt;&lt;/pre&gt; 最后，如果输入包含&quot;prompt&quot;文本提示，也会将其添加到输出字典中，然后返回处理后的输入数据 &lt;pre&gt;&lt;code&gt;        # 如果输入数据中包含提示信息
        if &quot;prompt&quot; in data:  
            # 添加提示信息到输入字典
            inputs[&quot;prompt&quot;] = data[&quot;prompt&quot;]  

        # 返回处理后的输入数据
        return inputs&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;整体而言，这个方法实现了从多样化的原始输入到标准化模型输入的转换流程，处理了数据格式转换、缺失数据补充、维度调整等核心问题，确保了Aloha策略模型能够接收一致的输入格式，从而实现稳定的推理和训练&lt;/p&gt; 
&lt;h4 id=&quot;4.3.3%C2%A0AlohaOutputs%EF%BC%9A%E5%AE%9A%E4%B9%89Aloha%20%E7%AD%96%E7%95%A5%E7%9A%84%E8%BE%93%E5%87%BA%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&quot; name=&quot;4.3.3%C2%A0AlohaOutputs%EF%BC%9A%E5%AE%9A%E4%B9%89Aloha%20%E7%AD%96%E7%95%A5%E7%9A%84%E8%BE%93%E5%87%BA%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&quot;&gt;2.3.3 AlohaOutputs：定义Aloha 策略的输出数据结构&lt;/h4&gt; 
&lt;p&gt;`AlohaOutputs` 类定义了 Aloha 策略的输出数据结构，同样使用 `dataclasses.dataclass` 装饰器&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;# 定义AlohaOutputs类，继承自transforms.DataTransformFn
class AlohaOutputs(transforms.DataTransformFn):  

    # 如果为True，将关节和夹持器值从标准Aloha空间转换为pi内部运行时使用的空间
    # pi内部运行时使用的空间用于训练基础模型
    adapt_to_pi: bool = True  # 是否适配pi内部运行时，默认为True&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;`__call__` 方法处理输出数据，仅返回前14个维度的动作数据，并进行必要的编码转换&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;    # 定义__call__方法，处理输出数据
    def __call__(self, data: dict) -&amp;gt; dict:  
        # 仅返回前14维的动作数据，即将动作数据转换为NumPy数组，并取前14维
        actions = np.asarray(data[&quot;actions&quot;][:, :14])  

        # 编码动作数据并返回字典
        return {&quot;actions&quot;: _encode_actions(actions, adapt_to_pi=self.adapt_to_pi)}  &lt;/code&gt;&lt;/pre&gt; 
&lt;h4 id=&quot;4.3.4%20%E5%A4%9A%E4%B8%AA%E8%BE%85%E5%8A%A9%E5%87%BD%E6%95%B0%EF%BC%9A%E6%95%B0%E6%8D%AE%E7%9A%84%E6%A0%87%E5%87%86%E5%8C%96%E3%80%81%E5%8F%8D%E6%A0%87%E5%87%86%E5%8C%96%E3%80%81%E5%85%B3%E8%8A%82%E8%A7%92%E5%BA%A6%E7%BF%BB%E8%BD%AC&quot; name=&quot;4.3.4%20%E5%A4%9A%E4%B8%AA%E8%BE%85%E5%8A%A9%E5%87%BD%E6%95%B0%EF%BC%9A%E6%95%B0%E6%8D%AE%E7%9A%84%E6%A0%87%E5%87%86%E5%8C%96%E3%80%81%E5%8F%8D%E6%A0%87%E5%87%86%E5%8C%96%E3%80%81%E5%85%B3%E8%8A%82%E8%A7%92%E5%BA%A6%E7%BF%BB%E8%BD%AC&quot;&gt;2.3.4 多个辅助函数：数据的标准化、反标准化、关节角度翻转&lt;/h4&gt; 
&lt;p&gt;此外，代码中还包含多个辅助函数，用于数据的标准化、反标准化、关节角度翻转、夹持器位置的线性和角度转换等&lt;br&gt; &lt;br&gt; 这些函数确保了数据在不同控制系统之间的兼容性和一致性&lt;/p&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86%20%E6%A8%A1%E5%9E%8B%E8%AE%AD%E7%BB%83%E7%9A%84%E9%85%8D%E7%BD%AE%EF%BC%9Asrc%E4%B8%8Btraining%E6%A8%A1%E5%9D%97%E7%9A%84%E5%85%A8%E9%9D%A2%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%A3%E8%AF%BB&quot; name=&quot;%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86%20%E6%A8%A1%E5%9E%8B%E8%AE%AD%E7%BB%83%E7%9A%84%E9%85%8D%E7%BD%AE%EF%BC%9Asrc%E4%B8%8Btraining%E6%A8%A1%E5%9D%97%E7%9A%84%E5%85%A8%E9%9D%A2%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%A3%E8%AF%BB&quot;&gt;第三部分 模型训练的配置：src下training模块的全面分析与解读&lt;/h2&gt; 
&lt;p&gt;training模块是 OpenPI 项目中负责训练相关功能的核心部分，该目录下包含了以下主要文件：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;checkpoints.py - 检查点管理&lt;/li&gt;
&lt;li&gt;config.py - 配置系统&lt;/li&gt;
&lt;li&gt;data_loader.py - 数据加载器&lt;/li&gt;
&lt;li&gt;data_loader_test.py - 数据加载器测试&lt;/li&gt;
&lt;li&gt;optimizer.py - 优化器实现&lt;/li&gt;
&lt;li&gt;sharding.py - 模型分片工具&lt;/li&gt;
&lt;li&gt;utils.py - 通用工具函数&lt;/li&gt;
&lt;li&gt;weight_loaders.py - 模型权重加载器&lt;/li&gt;
&lt;/ol&gt; 
&lt;h3 id=&quot;2.1%20%E9%85%8D%E7%BD%AE%E7%B3%BB%E7%BB%9F%20(config.py)&quot; name=&quot;2.1%20%E9%85%8D%E7%BD%AE%E7%B3%BB%E7%BB%9F%20(config.py)&quot;&gt;3.1 配置系统 (config.py)&lt;/h3&gt; 
&lt;p&gt;定义了训练过程的各种配置类型，包括：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`TrainConfig`：顶级训练配置，包含模型、数据、优化器等所有训练参数&lt;/li&gt;
&lt;li&gt;`DataConfigFactory`：抽象工厂类，用于创建特定环境的数据配置&lt;/li&gt;
&lt;li&gt;`AssetsConfig`：管理资产（如归一化统计数据）的位置&lt;/li&gt;
&lt;li&gt;预定义了多种常用配置（如 ALOHA、DROID、LIBERO 等环境的配置）&lt;/li&gt;
&lt;li&gt;通过 `get_config` 函数根据名称检索预定义配置&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;在配置流程上&lt;/p&gt; 
&lt;p&gt;   - 训练脚本通过 `_config.cli()` 或 `_config.get_config()` 获取配置&lt;br&gt;    - 配置系统加载预定义的训练参数，确定训练环境和模型参数&lt;br&gt;    - 数据配置通过工厂模式创建，根据不同环境（ALOHA、DROID 等）提供不同的预处理流程&lt;/p&gt; 
&lt;h4 id=&quot;2.1.1%20%E5%9F%BA%E7%A1%80%E9%85%8D%E7%BD%AE%E7%B1%BBAssetsConfig%E3%80%81DataConfig&quot; name=&quot;2.1.1%20%E5%9F%BA%E7%A1%80%E9%85%8D%E7%BD%AE%E7%B1%BBAssetsConfig%E3%80%81DataConfig&quot;&gt;3.1.1 基础配置类AssetsConfig、DataConfig&lt;/h4&gt; 
&lt;p&gt;一个是AssetsConfig&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;class AssetsConfig:
    &quot;&quot;&quot;用于确定数据pipeline所需资产(如归一化统计信息)的位置&quot;&quot;&quot;
    assets_dir: str | None = None      # 资产目录
    asset_id: str | None = None        # 资产ID&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;一个是DataConfig&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;@dataclasses.dataclass(frozen=True)
class DataConfig:
    repo_id: str | None = None            # 数据集仓库ID
    asset_id: str | None = None           # 资产ID
    norm_stats: dict[str, _transforms.NormStats] | None = None  # 归一化统计信息
    repack_transforms: _transforms.Group  # 数据重打包转换
    data_transforms: _transforms.Group    # 数据预处理转换
    model_transforms: _transforms.Group   # 模型特定转换&lt;/code&gt;&lt;/pre&gt; 
&lt;h4 id=&quot;2.1.2%20%E6%95%B0%E6%8D%AE%E9%9B%86%E9%85%8D%E7%BD%AE%EF%BC%9A%E5%8C%85%E5%90%ABALOHA%E3%80%81Libero%E4%B8%A4%E5%A5%97%E6%95%B0%E6%8D%AE%E9%9B%86&quot; name=&quot;2.1.2%20%E6%95%B0%E6%8D%AE%E9%9B%86%E9%85%8D%E7%BD%AE%EF%BC%9A%E5%8C%85%E5%90%ABALOHA%E3%80%81Libero%E4%B8%A4%E5%A5%97%E6%95%B0%E6%8D%AE%E9%9B%86&quot;&gt;3.1.2 数据集配置：包含ALOHA、Libero两套数据集——LeRobotLiberoDataConfig&lt;/h4&gt; 
&lt;p&gt;涉及两个配置&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;一个是LeRobotAlohaDataConfig &lt;pre&gt;&lt;code&gt;@dataclasses.dataclass(frozen=True)
class LeRobotAlohaDataConfig(DataConfigFactory):
    &quot;&quot;&quot;ALOHA数据集配置&quot;&quot;&quot;
    use_delta_joint_actions: bool = True      # 是否使用关节角度增量
    default_prompt: str | None = None         # 默认提示语
    adapt_to_pi: bool = True                  # 是否适配到π内部运行时&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;一个是LeRobotLiberoDataConfig &lt;pre&gt;&lt;code&gt;@dataclasses.dataclass(frozen=True)
class LeRobotLiberoDataConfig(DataConfigFactory):
    &quot;&quot;&quot;Libero数据集配置&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;p name=&quot;2.1.3%20%E8%AE%AD%E7%BB%83%E9%85%8D%E7%BD%AETrainConfig%EF%BC%9A%E6%A8%A1%E5%9E%8B%E3%80%81%E6%95%B0%E6%8D%AE%E3%80%81%E4%BC%98%E5%8C%96%E5%99%A8%E7%AD%89%E8%AE%AD%E7%BB%83%E5%8F%82%E6%95%B0%E7%9A%84%E8%AE%BE%E7%BD%AE&quot;&gt;对于后者的结构，详见下图&lt;/p&gt; 
 
&lt;ol&gt;
&lt;li&gt;`LeRobotLiberoDataConfig` 是一个用于机器人控制系统的数据配置类，它负责定义整个数据管道中不同阶段的数据转换操作。这个类通过 `@dataclasses.dataclass(frozen=True)` 装饰器声明为不可变数据类，确保配置一旦创建就不能被修改，增强了数据处理的稳定性&lt;/li&gt;
&lt;li&gt;该类重写了基类 `DataConfigFactory` 的 `create` 方法，该方法是整个配置系统的核心，负责构建完整的数据配置 &lt;pre&gt;&lt;code&gt;    def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -&amp;gt; DataConfig:
        # 重写父类方法，创建数据配置。参数包括资产目录路径和模型配置，返回DataConfig对象
        # ..&lt;/code&gt;&lt;/pre&gt; 方法接收两个关键参数：存放数据资产的目录路径和模型配置对象，然后返回一个完整的 `DataConfig` 对象&lt;/li&gt;
&lt;li&gt;在方法内部，首先定义了 `repack_transform`，这是一个仅在训练阶段应用的转换器，用于将数据集中的键名映射到推理环境期望的键名&lt;br&gt; 例如，将 `&quot;observation/image&quot;` 映射到 `&quot;image&quot;`。这种转换确保了训练数据和推理环境之间的一致性，是适配不同数据源的关键步骤&lt;/li&gt;
&lt;li&gt;接下来，`&lt;strong&gt;data_transforms&lt;/strong&gt;` 配置了同时应用于训练和推理阶段的转换操作&lt;br&gt; 它使用 `libero_policy.LiberoInputs` 处理输入数据，`libero_policy.LiberoOutputs` 处理输出数据 &lt;pre&gt;&lt;code&gt;        # 数据转换应用于来自数据集的数据和推理过程中的数据
        # 下面，定义了进入模型的数据转换（&quot;inputs&quot;）和从模型输出的数据转换（&quot;outputs&quot;）（后者仅在推理时使用）
        # 这些转换在`libero_policy.py`中定义
        # 一旦创建了自己的转换，你可以用自己的替换下面的转换
        data_transforms = _transforms.Group(
             # 定义输入转换，使用LiberoInputs处理器
            inputs=[libero_policy.LiberoInputs(action_dim=model_config.action_dim, model_type=model_config.model_type)], 

            # 定义输出转换，使用LiberoOutputs处理器
            outputs=[libero_policy.LiberoOutputs()],  
        )&lt;/code&gt;&lt;/pre&gt; 这些转换器负责将原始数据调整为模型能够处理的格式&lt;/li&gt;
&lt;li&gt;特别值得注意的是关于动作表示的转换：该配置支持将绝对动作（如具体的关节角度）转换为相对动作（相对于初始状态的变化量）&lt;br&gt; 通过 `&lt;strong&gt;delta_action_mask&lt;/strong&gt;` 创建一个布尔掩码，指定哪些动作维度需要进行转换（这里是前6个维度对应机器人关节，保留最后一个维度对应夹爪不变） &lt;pre&gt;&lt;code&gt;        # 创建动作掩码，指定哪些维度需要转换为相对动作（前6个关节），哪些保持绝对值（夹爪）
        # 创建布尔掩码，前6个维度为True，最后一个维度为False
        delta_action_mask = _transforms.make_bool_mask(6, -1)  &lt;/code&gt;&lt;/pre&gt; 这对于训练基于相对动作的模型（如Pi0模型）非常重要&lt;/li&gt;
&lt;li&gt;最后，`&lt;strong&gt;model_transforms&lt;/strong&gt;` 处理模型特有的转换操作，比如提示文本的token化和图像尺寸调整 &lt;pre&gt;&lt;code&gt;        # 使用模型配置创建模型转换——处理提示文本的token化和其他模型特定的转换
        model_transforms = ModelTransformFactory()(model_config)  &lt;/code&gt;&lt;/pre&gt; 这些转换由 `ModelTransformFactory` 根据模型类型动态创建，支持不同类型的模型（Pi0或Pi0_FAST）&lt;/li&gt;
&lt;li&gt;整个方法通过 `dataclasses.replace` 将这些转换器与基础配置（通过 `create_base_config` 创建）合并，生成最终的数据配置对象 &lt;pre&gt;&lt;code&gt;        return dataclasses.replace(
            self.create_base_config(assets_dirs),         # 创建基础配置
            repack_transforms=repack_transform,           # 设置重新打包转换
            data_transforms=data_transforms,              # 设置数据转换
            model_transforms=model_transforms,            # 设置模型转换
        )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;2.1.3%20%E8%AE%AD%E7%BB%83%E9%85%8D%E7%BD%AETrainConfig%EF%BC%9A%E6%A8%A1%E5%9E%8B%E3%80%81%E6%95%B0%E6%8D%AE%E3%80%81%E4%BC%98%E5%8C%96%E5%99%A8%E7%AD%89%E8%AE%AD%E7%BB%83%E5%8F%82%E6%95%B0%E7%9A%84%E8%AE%BE%E7%BD%AE&quot; name=&quot;2.1.3%20%E8%AE%AD%E7%BB%83%E9%85%8D%E7%BD%AETrainConfig%EF%BC%9A%E6%A8%A1%E5%9E%8B%E3%80%81%E6%95%B0%E6%8D%AE%E3%80%81%E4%BC%98%E5%8C%96%E5%99%A8%E7%AD%89%E8%AE%AD%E7%BB%83%E5%8F%82%E6%95%B0%E7%9A%84%E8%AE%BE%E7%BD%AE&quot;&gt;3.1.3 训练配置TrainConfig：模型、数据、优化器等训练参数的设置&lt;/h4&gt; 
&lt;pre&gt;&lt;code&gt;class TrainConfig:
    name: str                              # 配置名称
    project_name: str = &quot;openpi&quot;           # 项目名称
    exp_name: str                          # 实验名称
    model: _model.BaseModelConfig          # 模型配置
    batch_size: int = 32                   # 批次大小
    num_train_steps: int = 30_000          # 训练步数
    lr_schedule: _optimizer.LRScheduleConfig      # 学习率调度
    optimizer: _optimizer.OptimizerConfig         # 优化器配置&lt;/code&gt;&lt;/pre&gt; 
&lt;h4 id=&quot;2.1.4%20%E9%A2%84%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE%EF%BC%9A%E5%9F%BA%E4%BA%8EALOHA%E6%88%96Libero%E6%95%B0%E6%8D%AE%E9%9B%86%E5%BE%AE%E8%B0%83%CF%800&quot; name=&quot;2.1.4%20%E9%A2%84%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE%EF%BC%9A%E5%9F%BA%E4%BA%8EALOHA%E6%88%96Libero%E6%95%B0%E6%8D%AE%E9%9B%86%E5%BE%AE%E8%B0%83%CF%800&quot;&gt;3.1.4 预定义配置：基于ALOHA/Libero数据集微调π0——比如完成aloha_sim_transfer_cube_human&lt;/h4&gt; 
&lt;p&gt;文件最后定义了多个具体的训练配置：&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;比如ALOHA的 &lt;pre&gt;&lt;code&gt;TrainConfig(
    name=&quot;pi0_aloha_pen_uncap&quot;,      # 配置名称，反映模型和数据集
    model=pi0.Pi0Config(),           # 使用pi0模型配置
    data=LeRobotAlohaDataConfig(     # 使用LeRobotAloha数据集配置

        # 数据集仓库ID
        repo_id=&quot;physical-intelligence/aloha_pen_uncap_diverse&quot;,  

        # 资产配置
        assets=AssetsConfig(  
            # 资产目录
            assets_dir=&quot;s3://openpi-assets/checkpoints/pi0_base/assets&quot;,  
            # 资产ID
            asset_id=&quot;trossen&quot;,          
        ),
        # 默认提示语
        default_prompt=&quot;uncap the pen&quot;,  

        # 数据重打包转换
        repack_transforms=_transforms.Group(      
            inputs=[
                # 重打包转换
                _transforms.RepackTransform(      
                    {
                        &quot;images&quot;: {
                            # 高视角摄像头图像
                            &quot;cam_high&quot;: &quot;observation.images.cam_high&quot;,  

                            # 左手腕摄像头图像
                            &quot;cam_left_wrist&quot;: &quot;observation.images.cam_left_wrist&quot;,

                            # 右手腕摄像头图像  
                            &quot;cam_right_wrist&quot;: &quot;observation.images.cam_right_wrist&quot;,                  
                        },

                        # 机器人状态
                        &quot;state&quot;: &quot;observation.state&quot;,  

                        # 动作
                        &quot;actions&quot;: &quot;action&quot;,           
                    }
                )
            ]
        ),

        base_config=DataConfig(
            # 是否只使用本地数据集，False表示允许从Hugging Face下载
            local_files_only=False,  
        ),
    ),

    # 加载预训练权重
    weight_loader=weight_loaders.CheckpointWeightLoader(&quot;s3://openpi-assets/checkpoints/pi0_base/params&quot;),  

    # 训练步数为20,000步
    num_train_steps=20_000,  
),&lt;/code&gt;&lt;/pre&gt; 当然，这里面还涉及到ALOHA中一个仿真环境中的操作任务 &lt;pre&gt;&lt;code&gt;# 这个配置用于演示如何在简单的模拟环境中进行训练
TrainConfig(
    name=&quot;pi0_aloha_sim&quot;,          # 配置名称
    model=pi0.Pi0Config(),         # 使用pi0模型配置
    data=LeRobotAlohaDataConfig(   # 使用LeRobotAloha数据集配置

         # 数据集仓库ID
        repo_id=&quot;lerobot/aloha_sim_transfer_cube_human&quot;, 
        default_prompt=&quot;Transfer cube&quot;,      # 默认提示语
        use_delta_joint_actions=False,       # 是否使用关节角度增量
    ),
    weight_loader=weight_loaders.CheckpointWeightLoader(&quot;s3://openpi-assets/checkpoints/pi0_base/params&quot;),        # 加载预训练权重
    num_train_steps=20_000,                  # 训练步数为20,000步
),&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;再比如Libero的 &lt;pre&gt;&lt;code&gt;TrainConfig(
    # 更改名称以反映你的模型和数据集
    name=&quot;pi0_libero&quot;,
    
    # 在这里定义模型配置 - 这个例子中我们使用pi0作为模型架构并执行完整微调
    # 在后面的例子中我们会展示如何修改配置来执行低内存(LORA)微调
    # 以及如何使用pi0-FAST作为替代架构
    model=pi0.Pi0Config(),
    
    # 在这里定义要训练的数据集。这个例子中我们使用Libero数据集
    # 对于你自己的数据集，你可以更改repo_id指向你的数据集
    # 同时修改DataConfig以使用你为数据集创建的新配置
    data=LeRobotLiberoDataConfig(
        # 指定数据集的Hugging Face仓库ID
        repo_id=&quot;physical-intelligence/libero&quot;,
        
        # 基础配置设置
        base_config=DataConfig(
            # 是否只使用本地数据集，False表示允许从Hugging Face下载
            local_files_only=False,  
            
            # 这个标志决定是否从LeRobot数据集的task字段加载提示(即任务指令)
            # 如果设为True，提示将会出现在输入字典的prompt字段中
            # 推荐设置为True
            prompt_from_task=True,
        ),
    ),
    
    # 在这里定义要加载哪个预训练检查点来初始化模型
    # 这应该与你上面选择的模型配置匹配 - 即在这种情况下我们使用pi0基础模型
    weight_loader=weight_loaders.CheckpointWeightLoader(
        &quot;s3://openpi-assets/checkpoints/pi0_base/params&quot;
    ),
    
    # 在下面你可以定义其他超参数，如学习率、训练步数等
    # 查看TrainConfig类以获取完整的可用超参数列表
    num_train_steps=30_000,  # 设置训练步数为30,000步
),&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;h3 id=&quot;2.2%20%E6%95%B0%E6%8D%AE%E5%8A%A0%E8%BD%BD%E7%B3%BB%E7%BB%9F%20data_loader.py&quot; name=&quot;2.2%20%E6%95%B0%E6%8D%AE%E5%8A%A0%E8%BD%BD%E7%B3%BB%E7%BB%9F%20data_loader.py&quot;&gt;3.2 数据加载系统 data_loader.py&lt;/h3&gt; 
&lt;p&gt;定义了数据集和数据加载器的接口（`Dataset` 和 `DataLoader`）&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;实现了数据转换管道，将原始数据转换为模型可用的格式&lt;/li&gt;
&lt;li&gt;支持各种数据源：真实数据集（通过 LeRobot 数据集接口）、模拟数据（使用 `FakeDataset`）&lt;/li&gt;
&lt;li&gt;提供数据归一化和转换功能&lt;/li&gt;
&lt;/ol&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;在数据加载流程上&lt;/p&gt; 
 &lt;p&gt;TrainConfig&lt;br&gt;    └── data (DataConfigFactory)&lt;br&gt;        ├── create() → DataConfig&lt;br&gt;        │   ├── repo_id: 数据集 ID&lt;br&gt;        │   ├── norm_stats: 归一化统计数据&lt;br&gt;        │   ├── repack_transforms: 数据重包装转换&lt;br&gt;        │   ├── data_transforms: 特定于环境的转换&lt;br&gt;        │   └── model_transforms: 特定于模型的转换&lt;br&gt;        └── _load_norm_stats() → 归一化统计数据&lt;/p&gt; 
 &lt;p&gt;&lt;br&gt;    create_data_loader(config)&lt;br&gt;    ├── data_config = config.data.create()&lt;br&gt;    ├── dataset = create_dataset(data_config, config.model)&lt;br&gt;    ├── dataset = transform_dataset(dataset, data_config)&lt;br&gt;    └── return DataLoaderImpl(data_config, TorchDataLoader(...))&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;h4 id=&quot;2.2.1%20FakeDataset%E7%B1%BB&quot; name=&quot;2.2.1%20FakeDataset%E7%B1%BB&quot;&gt;3.2.1 FakeDataset类&lt;/h4&gt; 
&lt;h4 id=&quot;2.2.2%C2%A0create_dataset%EF%BC%9A%E5%88%9B%E5%BB%BA%E9%80%82%E5%90%88%E8%AE%AD%E7%BB%83%E7%9A%84%E6%95%B0%E6%8D%AE%E9%9B%86&quot; name=&quot;2.2.2%C2%A0create_dataset%EF%BC%9A%E5%88%9B%E5%BB%BA%E9%80%82%E5%90%88%E8%AE%AD%E7%BB%83%E7%9A%84%E6%95%B0%E6%8D%AE%E9%9B%86&quot;&gt;3.2.2 create_dataset：创建适合训练的数据集&lt;/h4&gt; 
&lt;p&gt;`create_dataset` 函数是一个关键的数据准备工具，负责根据配置参数创建适合模型训练的数据集。这个函数通过处理不同数据源和应用必要的转换，为模型提供标准化的训练数据。&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;首先，函数检查 `data_config.repo_id` 的值，这个参数指定了数据仓库的标识符 &lt;pre&gt;&lt;code&gt;def create_dataset(data_config: _config.DataConfig, model_config: _model.BaseModelConfig) -&amp;gt; Dataset:
    &quot;&quot;&quot;创建用于训练的数据集&quot;&quot;&quot;
    # 从数据配置中获取仓库ID
    repo_id = data_config.repo_id&lt;/code&gt;&lt;/pre&gt; 如果 `repo_id` 为 `None`，函数会抛出 `ValueError` 异常，明确指出无法创建数据集。这是一种防御性编程的体现，确保基本的配置参数存在 &lt;pre&gt;&lt;code&gt;    # 如果仓库ID为空，抛出错误
    if repo_id is None:
        raise ValueError(&quot;Repo ID is not set. Cannot create dataset.&quot;)&lt;/code&gt;&lt;/pre&gt; 如果 `repo_id` 的值为 &quot;fake&quot;，函数则创建并返回一个 `FakeDataset` 实例，其样本数设为 1024。这种虚拟数据集在测试模型架构、调试训练流程或者进行性能基准测试时非常有用，无需加载真实数据即可快速验证系统功能 &lt;pre&gt;&lt;code&gt;    # 如果是fake数据集，返回包含1024个样本的假数据集
    if repo_id == &quot;fake&quot;:
        return FakeDataset(model_config, num_samples=1024)&lt;/code&gt;&lt;/pre&gt; 对于其他情况（即使用真实数据），函数首先创建 `LeRobotDatasetMetadata` 对象来获取数据集的元信息 &lt;pre&gt;&lt;code&gt;    # 创建数据集元数据对象，包含数据集的基本信息（如fps等）
    dataset_meta = lerobot_dataset.LeRobotDatasetMetadata(
        repo_id, 
        local_files_only=data_config.local_files_only
    )&lt;/code&gt;&lt;/pre&gt; 然后初始化 `&lt;span style=&quot;color:#fe2c24&quot;&gt;LeRobotDataset&lt;/span&gt;` 实例 &lt;pre&gt;&lt;code&gt;    # 创建LeRobot数据集实例
    dataset = lerobot_dataset.LeRobotDataset(
        data_config.repo_id,
        # 创建时间戳字典，用于采样动作序列
        delta_timestamps={
            # 对每个动作序列键，根据模型的动作视界长度和数据集的fps生成时间戳列表
            key: [t / dataset_meta.fps for t in range(model_config.action_horizon)]
            for key in data_config.action_sequence_keys
        },
        # 是否只使用本地文件
        local_files_only=data_config.local_files_only,
    )&lt;/code&gt;&lt;/pre&gt; 特别值得注意的是，函数会根据模型的 `action_horizon`（动作预测的时间步长）和数据集的帧率（fps）计算 `delta_timestamps`，这些时间戳用于在时序数据中定位动作序列。这种计算确保了动作序列的时间间隔与模型预期一致，无论原始数据的采样率如何&lt;/li&gt;
&lt;li&gt;最后，如果 `data_config.prompt_from_task` 设置为 `True`，函数会将原始数据集包装在 `TransformedDataset` 中，并应用 `PromptFromLeRobotTask` 转换 &lt;pre&gt;&lt;code&gt;    # 如果配置指定从任务中提取提示信息
    if data_config.prompt_from_task:
        # 创建转换后的数据集，应用PromptFromLeRobotTask转换，将任务描述转换为提示
        dataset = TransformedDataset(
            dataset, 
            [_transforms.PromptFromLeRobotTask(dataset_meta.tasks)]
        )&lt;/code&gt;&lt;/pre&gt; 这个转换可能将任务描述转换为自然语言提示，增强模型对任务上下文的理解能力&lt;br&gt; 然后返回处理好的数据集 &lt;pre&gt;&lt;code&gt;    # 返回处理后的数据集
    return dataset&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;2.2.3%C2%A0transform_dataset%EF%BC%9A%E5%AF%B9%E6%95%B0%E6%8D%AE%E9%9B%86%E5%BA%94%E7%94%A8%E8%BD%AC%E6%8D%A2%EF%BC%8C%E6%AF%94%E5%A6%82%E6%95%B0%E6%8D%AE%E6%B8%85%E6%B4%97%E7%AD%89(%E5%88%9B%E5%BB%BATransformedDataset%E5%AE%9E%E4%BE%8B)&quot; name=&quot;2.2.3%C2%A0transform_dataset%EF%BC%9A%E5%AF%B9%E6%95%B0%E6%8D%AE%E9%9B%86%E5%BA%94%E7%94%A8%E8%BD%AC%E6%8D%A2%EF%BC%8C%E6%AF%94%E5%A6%82%E6%95%B0%E6%8D%AE%E6%B8%85%E6%B4%97%E7%AD%89(%E5%88%9B%E5%BB%BATransformedDataset%E5%AE%9E%E4%BE%8B)&quot;&gt;3.2.3 transform_dataset：对数据集应用转换，比如数据清洗等(创建TransformedDataset实例)&lt;/h4&gt; 
&lt;p&gt;`transform_dataset` 函数是数据预处理管道中的关键组件，负责对原始数据集应用一系列转换操作，以满足模型训练的需求。该函数接收一个原始数据集、数据配置对象以及一个可选的控制标志，并返回经过转换的新数据集&lt;/p&gt; 
&lt;p&gt;首先，函数会处理数据归一化统计信息（normalization statistics）。对于实际数据集（非&quot;fake&quot;数据集），如果没有显式跳过归一化统计（`skip_norm_stats=False`），函数会检查数据配置中是否包含必要的归一化统计数据。如果这些统计数据缺失，函数会抛出一个明确的错误信息，提示用户需要运行特定脚本来计算这些统计数据。这种检查机制确保了数据归一化步骤能够正确执行，避免了训练过程中可能出现的数值问题&lt;/p&gt; 
&lt;p&gt;核心转换逻辑通过创建一个 `TransformedDataset` 实例来实现，该实例封装了原始数据集和一系列转换函数。这些转换函数按照特定顺序应用：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;首先是数据重新打包转换（`repack_transforms`），可能用于调整数据的基本结构&lt;/li&gt;
&lt;li&gt;接着是一般数据转换（`data_transforms`），处理数据清洗、增强等操作&lt;/li&gt;
&lt;li&gt;然后应用归一化转换（`Normalize`），使用前面获取的统计数据&lt;/li&gt;
&lt;li&gt;最后是模型特定的转换（`model_transforms`），针对特定模型架构的数据格式要求&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;2.2.4%C2%A0create_data_loader%EF%BC%9A%E5%88%9B%E5%BB%BA%E7%94%A8%E4%BA%8E%E8%AE%AD%E7%BB%83%E7%9A%84%E6%95%B0%E6%8D%AE%E5%8A%A0%E8%BD%BD%E5%99%A8&quot; name=&quot;2.2.4%C2%A0create_data_loader%EF%BC%9A%E5%88%9B%E5%BB%BA%E7%94%A8%E4%BA%8E%E8%AE%AD%E7%BB%83%E7%9A%84%E6%95%B0%E6%8D%AE%E5%8A%A0%E8%BD%BD%E5%99%A8&quot;&gt;3.2.4 create_data_loader：创建用于训练的数据加载器&lt;/h4&gt; 
&lt;p&gt;`create_data_loader` 函数是整个数据处理流水线的核心组件，它协调多个模块共同工作，创建一个用于模型训练的数据加载器&lt;/p&gt; 
&lt;p&gt;整个函数的工作流程可以分为三个主要阶段：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;第一阶段：数据集准备&lt;/strong&gt;&lt;br&gt; 函数首先通过调用 `data_config.create()` 方法创建数据配置对象，该对象包含了所有数据处理相关的配置信息&lt;br&gt; &lt;br&gt; 随后，通过 `create_dataset` 函数创建原始数据集，这可能是一个真实的机器人数据集或者是一个用于测试的假数据集（当 `repo_id` 为 &quot;fake&quot; 时）&lt;br&gt; &lt;br&gt; 然后，调用 `transform_dataset` 函数应用一系列数据转换，包括数据重新打包、数据清洗、归一化和模型特定转换。这些转换确保了原始数据被正确处理为模型所需的格式&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;第二阶段：PyTorch 数据加载器创建&lt;/strong&gt;&lt;br&gt; 接下来，函数实例化一个 `TorchDataLoader` 对象，这是对 PyTorch 数据加载器的封装。这个过程涉及多个关键参数设置：计算各进程的本地批量大小（通过全局批量大小除以进程数）&lt;br&gt; 配置数据分片策略（sharding）用于分布式训练&lt;br&gt; 设置是否打乱数据、工作进程数和随机种子等&lt;br&gt; &lt;br&gt; `TorchDataLoader` 的设计支持无限迭代数据（当 `num_batches` 为 `None` 时）或限定批次数的迭代，这对于训练和评估场景都很适用。其内部使用 JAX 的分片机制确保数据在分布式环境中正确分布&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;第三阶段：接口适配器实现&lt;/strong&gt;&lt;br&gt; 最后，函数通过定义嵌套类 `DataLoaderImpl` 来适配 `DataLoader` 协议接口。这个类封装了前面创建的 `TorchDataLoader` 实例，并提供了两个关键方法：&lt;br&gt; 1. `data_config()` 返回数据配置信息，便于训练代码访问数据处理的元信息&lt;br&gt; &lt;br&gt; 2. `__iter__()` 生成器方法对数据批次进行最后的格式转换：&lt;br&gt; 将字典格式的观察数据转换为结构化的 `Observation` 对象（通过 `Observation.from_dict`）提取动作数据&lt;br&gt; 以元组形式 `(observation, actions)` 返回每个批次&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;这种设计实现了关注点分离，使数据加载、转换和格式适配各自独立，同时又协同工作，为模型训练提供了一个干净的数据流接口。函数还处理了多进程环境、数据分片和内存效率等复杂问题，这些都是大规模机器学习训练中的关键挑战&lt;/p&gt; 
&lt;h3 id=&quot;2.3%20%E4%BC%98%E5%8C%96%E5%99%A8%E7%B3%BB%E7%BB%9F%20(optimizer.py)&quot; name=&quot;2.3%20%E4%BC%98%E5%8C%96%E5%99%A8%E7%B3%BB%E7%BB%9F%20(optimizer.py)&quot;&gt;3.3 优化器系统 (optimizer.py)&lt;/h3&gt; 
&lt;p&gt;定义了多种学习率调度策略：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`CosineDecaySchedule`：余弦衰减学习率&lt;/li&gt;
&lt;li&gt;`RsqrtDecaySchedule`：反平方根衰减学习率&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;实现了常用优化器配置：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`AdamW`：带有权重衰减的 Adam 优化器&lt;/li&gt;
&lt;li&gt;`SGD`：随机梯度下降优化器&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;通过 `create_optimizer` 函数统一创建优化器实例&lt;/p&gt; 
&lt;h3 id=&quot;2.4%20%E6%A3%80%E6%9F%A5%E7%82%B9%E7%B3%BB%E7%BB%9F%20(checkpoints.py)&quot; name=&quot;2.4%20%E6%A3%80%E6%9F%A5%E7%82%B9%E7%B3%BB%E7%BB%9F%20(checkpoints.py)&quot;&gt;3.4 检查点系统 (checkpoints.py)&lt;/h3&gt; 
&lt;p&gt;负责模型状态的保存和恢复，比如管理训练状态的序列化，包括：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;模型参数&lt;/li&gt;
&lt;li&gt;优化器状态&lt;/li&gt;
&lt;li&gt;EMA 参数（如果使用）&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;且使用 Orbax 库实现高效的检查点存储&lt;/p&gt; 
&lt;table border=&quot;1&quot; cellpadding=&quot;1&quot; cellspacing=&quot;1&quot; style=&quot;width:950px&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;模型初始化流程&lt;/td&gt;
&lt;td&gt;训练步骤流程&lt;/td&gt;
&lt;td&gt;与 models 模块的交互&lt;/td&gt;
&lt;td&gt;检查点管理流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;   init_train_state(config, rng, mesh)&lt;br&gt;    ├── 创建模型：model = config.model.create(rng)&lt;br&gt;    ├── 加载权重：partial_params = config.weight_loader.load(params)&lt;br&gt;    ├── 设置冻结参数：params = state_map(params, config.freeze_filter, ...)&lt;br&gt;    ├── 创建优化器：tx = create_optimizer(config.optimizer, config.lr_schedule)&lt;br&gt;    └── 返回 TrainState&lt;/td&gt;
&lt;td&gt;   train_step(config, rng, state, batch)&lt;br&gt;    ├── 计算梯度：loss, grads = value_and_grad(model.&lt;span style=&quot;color:#511b78&quot;&gt;***pute_loss&lt;/span&gt;)()&lt;br&gt;    ├── 更新参数：updates, new_opt_state = state.tx.update(grads, state.opt_state, params)&lt;br&gt;    ├── 应用更新：new_params = optax.apply_updates(params, updates)&lt;br&gt;    ├── 更新 EMA 参数（如果配置）&lt;br&gt;    └── 返回 new_state, info&lt;/td&gt;
&lt;td&gt;   - 训练系统加载模型定义 (`BaseModel`)&lt;br&gt;    - 处理模型参数的保存和加载&lt;br&gt;    - 调用模型的 `***pute_loss` 方法计算损失——&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;详见上文的「1.2.4.3 损失函数 `***pute_loss`」&lt;/em&gt;&lt;/span&gt;
&lt;/td&gt;
&lt;td&gt;   save_state(checkpoint_manager, state, data_loader, step)&lt;br&gt;    ├── _split_params(state) → 分离训练状态和推理参数&lt;br&gt;    ├── 保存归一化统计数据到 assets 目录&lt;br&gt;    └── checkpoint_manager.save() → 保存检查点&lt;br&gt;    &lt;br&gt;    restore_state(checkpoint_manager, state, data_loader)&lt;br&gt;    ├── checkpoint_manager.restore() → 恢复检查点&lt;br&gt;    └── _merge_params() → 合并恢复的参数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;2.5%20%E6%A8%A1%E5%9E%8B%E5%88%86%E7%89%87%E7%B3%BB%E7%BB%9F(sharding.py)%EF%BC%9A%E5%90%ABFSDP%E7%9A%84%E5%AE%9E%E7%8E%B0&quot; name=&quot;2.5%20%E6%A8%A1%E5%9E%8B%E5%88%86%E7%89%87%E7%B3%BB%E7%BB%9F(sharding.py)%EF%BC%9A%E5%90%ABFSDP%E7%9A%84%E5%AE%9E%E7%8E%B0&quot;&gt;3.5 模型分片系统(sharding.py)：含FSDP的实现&lt;/h3&gt; 
&lt;p&gt;实现分布式训练时的模型参数分片&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;提供 `fsdp_sharding` 函数用于全参数数据并行(FSDP)的实现&lt;/li&gt;
&lt;li&gt;基于 JAX 的分片机制，优化大规模模型的训练性能&lt;/li&gt;
&lt;li&gt;通过 `activation_sharding_constraint` 处理激活值的分片&lt;/li&gt;
&lt;/ol&gt; 
&lt;h3 id=&quot;2.6%20%E6%9D%83%E9%87%8D%E5%8A%A0%E8%BD%BD%E7%B3%BB%E7%BB%9F%20(weight_loaders.py)&quot; name=&quot;2.6%20%E6%9D%83%E9%87%8D%E5%8A%A0%E8%BD%BD%E7%B3%BB%E7%BB%9F%20(weight_loaders.py)&quot;&gt;3.6 权重加载系统 (weight_loaders.py)&lt;/h3&gt; 
&lt;p&gt;定义了 `WeightLoader` 协议，用于加载预训练权重，且实现了多种加载策略：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;`NoOpWeightLoader`：不加载权重（用于从头训练）&lt;/li&gt;
&lt;li&gt;`CheckpointWeightLoader`：从检查点加载完整权重&lt;/li&gt;
&lt;li&gt;`PaliGemmaWeightLoader`：从官方 PaliGemma 检查点加载权重&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;另，还支持权重合并功能，可以部分加载权重（如 LoRA 微调）&lt;/p&gt; 
&lt;h3 id=&quot;2.7%20%E8%BE%85%E5%8A%A9%E5%B7%A5%E5%85%B7(utils.py)&quot; name=&quot;2.7%20%E8%BE%85%E5%8A%A9%E5%B7%A5%E5%85%B7(utils.py)&quot;&gt;3.7 辅助工具(utils.py)&lt;/h3&gt; 
&lt;p&gt;定义了 `TrainState` 数据类，封装了训练过程的状态&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;提供日志记录和调试功能&lt;/li&gt;
&lt;li&gt;实现了 PyTree 转换和可视化功能&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E7%AC%AC%E4%B8%89%E9%83%A8%E5%88%86%20%E6%A8%A1%E5%9E%8B%E7%9A%84%E8%AE%AD%E7%BB%83%E4%B8%8E%E9%83%A8%E7%BD%B2%EF%BC%9A%E5%9F%BA%E4%BA%8E%E5%AE%A2%E6%88%B7%E7%AB%AF-%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%9E%B6%E6%9E%84&quot; name=&quot;%E7%AC%AC%E4%B8%89%E9%83%A8%E5%88%86%20%E6%A8%A1%E5%9E%8B%E7%9A%84%E8%AE%AD%E7%BB%83%E4%B8%8E%E9%83%A8%E7%BD%B2%EF%BC%9A%E5%9F%BA%E4%BA%8E%E5%AE%A2%E6%88%B7%E7%AB%AF-%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%9E%B6%E6%9E%84&quot;&gt;第四部分 模型的训练与部署：基于客户端-服务器C/S架构——openpi-Client/Scripts&lt;/h2&gt; 
&lt;p&gt;packages/openpi-client，是一个独立的客户端库openpi-client 库，主要负责：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;提供与策略服务器通信的接口：使用 WebSocketClientPolicy 连接服务器&lt;/li&gt;
&lt;li&gt;处理观察数据(图像、状态等)的发送，和动作数据的接收&lt;/li&gt;
&lt;li&gt;管理客户端运行时环境&lt;/li&gt;
&lt;li&gt;被各种机器人平台(如 ALOHA、DROID)使用来与服务器交互&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;scripts这个模块提供了服务器端的各种工具和脚本，主要包括：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;策略服务相关——serve_policy.py：启动策略服务器，处理来自客户端的请求&lt;/li&gt;
&lt;li&gt;训练相关——train.py: 模型训练的入口点&lt;/li&gt;
&lt;li&gt;数据处理——***pute_norm_stats.py: 计算数据归一化统计信息&lt;/li&gt;
&lt;li&gt;部署相关：提供 Docker 相关的配置和安装脚本&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;总的来说，这是一个典型的分布式系统设计：packages/openpi-client 提供轻量级的客户端接口，而 scripts/ 则提供服务器端的功能实现，两者通过 WebSocket 协议进行通信，形成了一个完整的策略部署和执行系统&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;所谓客户端-服务器架构——Client-server model，也称C/S架构、主从zòng式架构，是一种将客户端与服务器分割开来的分布式架构。&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;每一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。有很多不同类型的服务器，例如文件服务器、游戏服务器等&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
  
 &lt;hr&gt; 
 &lt;p&gt;客户端的特征：&lt;/p&gt; 
 &lt;ol&gt;
&lt;li&gt;主动的角色（主）&lt;/li&gt;
&lt;li&gt;发送请求&lt;/li&gt;
&lt;li&gt;等待直到收到响应&lt;/li&gt;
&lt;/ol&gt; 
 &lt;p&gt;服务端的特征：&lt;/p&gt; 
 &lt;ol&gt;
&lt;li&gt;被动的角色（从）&lt;/li&gt;
&lt;li&gt;等待来自客户端的请求&lt;/li&gt;
&lt;li&gt;处理请求并传回结果&lt;/li&gt;
&lt;/ol&gt; 
&lt;/blockquote&gt; 
&lt;h3 id=&quot;3.1%20packages%2Fopenpi-client%EF%BC%9A%E5%B8%AE%E7%9C%9F%E6%9C%BA%E6%88%96Sim%E4%B8%8E%E7%AD%96%E7%95%A5%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%9B%E8%A1%8C%E9%80%9A%E4%BF%A1%E5%92%8C%E4%BA%A4%E4%BA%92&quot; name=&quot;3.1%20packages%2Fopenpi-client%EF%BC%9A%E5%B8%AE%E7%9C%9F%E6%9C%BA%E6%88%96Sim%E4%B8%8E%E7%AD%96%E7%95%A5%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%9B%E8%A1%8C%E9%80%9A%E4%BF%A1%E5%92%8C%E4%BA%A4%E4%BA%92&quot;&gt;4.1 packages/openpi-client：帮真机或Sim与策略服务器进行通信和交互&lt;/h3&gt; 
&lt;p&gt;该模块的目录结构如下&lt;/p&gt; 
 
&lt;p&gt;这个客户端包的设计非常模块化，具有良好的扩展性，主要用于：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;连接到 OpenPI 服务器&lt;/li&gt;
&lt;li&gt;处理观察数据和动作序列&lt;/li&gt;
&lt;li&gt;管理机器人或仿真环境的运行&lt;/li&gt;
&lt;li&gt;提供事件监控和记录功能&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;它的设计允许在不同的机器人平台上灵活部署，支持实时控制和异步通信，是 OpenPI 项目中连接模型服务器和实际机器人执行系统的重要桥梁&lt;/p&gt; 
&lt;h4 id=&quot;3.1.1%20%E6%A0%B8%E5%BF%83%E6%8E%A5%E5%8F%A3%E5%B1%82&quot; name=&quot;3.1.1%20%E6%A0%B8%E5%BF%83%E6%8E%A5%E5%8F%A3%E5%B1%82&quot;&gt;4.1.1 核心接口层&lt;/h4&gt; 
&lt;p&gt;`BasePolicy`: 定义策略接口&lt;br&gt; `Environment`: 定义环境接口&lt;br&gt; `Agent`: 定义代理接口&lt;/p&gt; 
&lt;h4 id=&quot;3.1.2%20%E9%80%9A%E4%BF%A1%E5%B1%82&quot; name=&quot;3.1.2%20%E9%80%9A%E4%BF%A1%E5%B1%82&quot;&gt;4.1.2 通信层WebsocketClientPolicy&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;`WebsocketClientPolicy`: 实现与服务器的 WebSocket 通信&lt;/li&gt;
&lt;li&gt;`msgpack_numpy`: 处理数据序列化&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;3.1.3%20%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86%E5%B1%82&quot; name=&quot;3.1.3%20%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86%E5%B1%82&quot;&gt;4.1.3 数据处理层&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;`ActionChunkBroker`: 处理动作序列的分块和缓存&lt;/li&gt;
&lt;li&gt;`image_tools`: 提供图像处理和优化功能&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;3.1.4%20%E8%BF%90%E8%A1%8C%E6%97%B6%E7%B3%BB%E7%BB%9F%E5%B1%82&quot; name=&quot;3.1.4%20%E8%BF%90%E8%A1%8C%E6%97%B6%E7%B3%BB%E7%BB%9F%E5%B1%82&quot;&gt;4.1.4 运行时系统层&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;`Runtime`: 核心运行时系统&lt;/li&gt;
&lt;li&gt;`Subscriber`: 事件订阅系统&lt;/li&gt;
&lt;li&gt;`agents`: 具体代理实现&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;3.1.5%20%E5%B7%A5%E5%85%B7%E6%94%AF%E6%8C%81&quot; name=&quot;3.1.5%20%E5%B7%A5%E5%85%B7%E6%94%AF%E6%8C%81&quot;&gt;4.1.5 工具支持&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;图像处理工具&lt;/li&gt;
&lt;li&gt;数据类型转换&lt;/li&gt;
&lt;li&gt;网络通信优化&lt;/li&gt;
&lt;/ol&gt; 
&lt;h3 id=&quot;1.3%C2%A0scripts%EF%BC%9A%E5%8C%85%E5%90%AB%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86%E3%80%81%E6%A8%A1%E5%9E%8B%E8%AE%AD%E7%BB%83%2F%E6%8E%A8%E7%90%86%E7%9A%84%E5%A4%9A%E4%B8%AA%E8%84%9A%E6%9C%AC&quot; name=&quot;1.3%C2%A0scripts%EF%BC%9A%E5%8C%85%E5%90%AB%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86%E3%80%81%E6%A8%A1%E5%9E%8B%E8%AE%AD%E7%BB%83%2F%E6%8E%A8%E7%90%86%E7%9A%84%E5%A4%9A%E4%B8%AA%E8%84%9A%E6%9C%AC&quot;&gt;4.2 scripts(策略服务器)：包含数据处理、模型训练、模型推理的多个脚本&lt;/h3&gt; 
&lt;p&gt;根据下图&lt;/p&gt; 
 
&lt;p&gt;可知，scripts 目录包含多个 Python 脚本，这些脚本用于数据处理、模型训练和服务部署等任务，每个脚本通常对应一个特定的功能或任务&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;__init__.py&lt;/li&gt;
&lt;li&gt;***pute_norm_stats.py: 计算数据的归一化统计信息&lt;/li&gt;
&lt;li&gt;serve_policy.py：启动策略服务，提供模型推理接口&lt;br&gt; &lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;总之，serve_policy.py 是 openpi 中的策略推理服务端脚本，作用为：启动一个 WebSocket 服务器，加载预训练策略模型，等待外部请求（如来自 main.py 的控制程序），然后执行动作推理并返回结果&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;strong&gt;&lt;span style=&quot;color:#511b78&quot;&gt;说白了，将一个 Pi0 策略模型部署为网络服务（WebSocket API），供机器人主控进程远程调用&lt;/span&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;train_test.py: 训练和测试模型&lt;/li&gt;
&lt;li&gt;train.py: 训练模型&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4 id=&quot;1.3.1%20__init__.py&quot; name=&quot;1.3.1%20__init__.py&quot;&gt;4.2.1 __init__.py&lt;/h4&gt; 
&lt;h4 id=&quot;1.3.2%C2%A0***pute_norm_stats.py%EF%BC%9A%E8%AE%A1%E7%AE%97%E6%95%B0%E6%8D%AE%E7%9A%84%E5%BD%92%E4%B8%80%E5%8C%96%E7%BB%9F%E8%AE%A1%E4%BF%A1%E6%81%AF&quot; name=&quot;1.3.2%C2%A0***pute_norm_stats.py%EF%BC%9A%E8%AE%A1%E7%AE%97%E6%95%B0%E6%8D%AE%E7%9A%84%E5%BD%92%E4%B8%80%E5%8C%96%E7%BB%9F%E8%AE%A1%E4%BF%A1%E6%81%AF&quot;&gt;4.2.2 ***pute_norm_stats.py：计算数据的归一化统计信息&lt;/h4&gt; 
&lt;h4 id=&quot;1.3.3%C2%A0serve_policy.py%EF%BC%9A%E5%90%AF%E5%8A%A8%E7%AD%96%E7%95%A5%E6%9C%8D%E5%8A%A1%EF%BC%8C%E7%94%A8%E4%BA%8E%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86&quot; name=&quot;1.3.3%C2%A0serve_policy.py%EF%BC%9A%E5%90%AF%E5%8A%A8%E7%AD%96%E7%95%A5%E6%9C%8D%E5%8A%A1%EF%BC%8C%E7%94%A8%E4%BA%8E%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86&quot;&gt;4.2.3(上) serve_policy.py：启动策略服务，用于模型推理——且支持定义特定任务的文本指令prompt&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;在这个代码片段中，首先导入了一些必要的模块和库，包括 `policy`、`policy_config`、`websocket_policy_server` 和 `config`，这些模块来自 `openpi` 项目 &lt;pre&gt;&lt;code&gt;from openpi.policies import policy as _policy       # 导入 openpi.policies.policy 模块并重命名为 _policy
from openpi.policies import policy_config as _policy_config  # 导入 openpi.policies.policy_config 模块并重命名为 _policy_config
from openpi.serving import websocket_policy_server  # 导入 openpi.serving.websocket_policy_server 模块
from openpi.training import config as _config       # 导入 openpi.training.config 模块并重命名为 _config&lt;/code&gt;&lt;/pre&gt; 接下来定义了一个枚举类 `&lt;span style=&quot;color:#ed7976&quot;&gt;EnvMode&lt;/span&gt;`，&lt;em&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;它表示支持的环境类型，包括 `ALOHA`、`ALOHA_SIM`、`DROID` 和 `LIBERO`&lt;/span&gt;&lt;/em&gt; &lt;pre&gt;&lt;code&gt;class EnvMode(enum.Enum):
    &quot;&quot;&quot;支持的环境。&quot;&quot;&quot;
    ALOHA = &quot;aloha&quot;              # ALOHA 环境
    ALOHA_SIM = &quot;aloha_sim&quot;      # ALOHA 模拟环境
    DROID = &quot;droid&quot;              # DROID 环境
    LIBERO = &quot;libero&quot;            # LIBERO 环境&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;然后定义了几个数据类&lt;br&gt; `Checkpoint` 类用于从训练好的检查点加载策略，包含两个字段：`config`（训练配置名称）和 `dir`（检查点目录）&lt;br&gt; `Default` 类表示使用默认策略&lt;br&gt; `Args` 类定义了脚本的参数，包括环境类型、&lt;span style=&quot;color:#1c7331&quot;&gt;默认prompt&lt;/span&gt;、端口、是否记录策略行为以及如何加载策略 &lt;pre&gt;&lt;code&gt;@dataclasses.dataclass
class Args:
    &quot;&quot;&quot;Arguments for the serve_policy script.&quot;&quot;&quot;

    # Environment to serve the policy for. This is only used when serving default policies.
    env: EnvMode = EnvMode.ALOHA_SIM

    # If provided, will be used in case the &quot;prompt&quot; key is not present in the data, or if the model doesn&#039;t have a default
    # prompt.
    default_prompt: str | None = None

    # Port to serve the policy on.
    port: int = 8000
    # Record the policy&#039;s behavior for debugging.
    record: bool = False

    # Specifies how to load the policy. If not provided, the default policy for the environment will be used.
    policy: Checkpoint | Default = dataclasses.field(default_factory=Default)&lt;/code&gt;&lt;/pre&gt; 相当于如果你想&lt;strong&gt;&lt;span style=&quot;color:#1c7331&quot;&gt;定义你的特定任务指令prompt，则可以修改上面代码中的default_prompt&lt;/span&gt;&lt;/strong&gt;&lt;br&gt; &lt;br&gt; 接下来定义了一个字典 `DEFAULT_CHECKPOINT`，它&lt;u&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;为每个环境类型指定了默认的检查点配置&lt;/em&gt;&lt;/span&gt;&lt;/u&gt; &lt;pre&gt;&lt;code&gt;# 每个环境应使用的默认检查点
DEFAULT_CHECKPOINT: dict[EnvMode, Checkpoint] = {
    EnvMode.ALOHA: Checkpoint(
        config=&quot;pi0_aloha&quot;,
        dir=&quot;s3://openpi-assets/checkpoints/pi0_base&quot;,
    ),
    EnvMode.ALOHA_SIM: Checkpoint(
        config=&quot;pi0_aloha_sim&quot;,
        dir=&quot;s3://openpi-assets/checkpoints/pi0_aloha_sim&quot;,
    ),
    EnvMode.DROID: Checkpoint(
        config=&quot;pi0_fast_droid&quot;,
        dir=&quot;s3://openpi-assets/checkpoints/pi0_fast_droid&quot;,
    ),
    EnvMode.LIBERO: Checkpoint(
        config=&quot;pi0_fast_libero&quot;,
        dir=&quot;s3://openpi-assets/checkpoints/pi0_fast_libero&quot;,
    ),
}&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;span style=&quot;color:#be191c&quot;&gt;加载策略模型&lt;/span&gt;&lt;/strong&gt;&lt;br&gt; `create_default_policy` 函数根据环境类型创建默认策略，如果环境类型不支持，则抛出异常 &lt;pre&gt;&lt;code&gt;def create_default_policy(env: EnvMode, *, default_prompt: str | None = None) -&amp;gt; _policy.Policy:
    &quot;&quot;&quot;为给定环境创建默认策略 &quot;&quot;&quot;
    if checkpoint := DEFAULT_CHECKPOINT.get(env):              # 获取环境对应的默认检查点
        return _policy_config.create_trained_policy(
            _config.get_config(checkpoint.config), checkpoint.dir, default_prompt=default_prompt
        )  # 创建训练好的策略
    raise ValueError(f&quot;Unsupported environment mode: {env}&quot;)   # 如果环境不支持，抛出异常&lt;/code&gt;&lt;/pre&gt; `create_policy` 函数根据传入的参数创建策略，如果参数中指定了检查点，则从检查点加载策略，否则使用默认策略 &lt;pre&gt;&lt;code&gt;def create_policy(args: Args) -&amp;gt; _policy.Policy:
    &quot;&quot;&quot;根据给定的参数创建策略 &quot;&quot;&quot;
    match args.policy:          # 匹配策略类型
        case Checkpoint():      # 如果是 Checkpoint 类型
            return _policy_config.create_trained_policy(
                _config.get_config(args.policy.config), args.policy.dir, default_prompt=args.default_prompt
            )      # 创建训练好的策略
        case Default():          # 如果是 Default 类型
            return create_default_policy(args.env, default_prompt=args.default_prompt)      # 创建默认策略&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;span style=&quot;color:#be191c&quot;&gt;启动推理服务&lt;/span&gt;&lt;/strong&gt;&lt;br&gt; `main` 函数是脚本的入口点，它首先调用 `create_policy` 函数创建策略，然后记录策略的元数据 &lt;pre&gt;&lt;code&gt;def main(args: Args) -&amp;gt; None:
    policy = create_policy(args)           # 创建策略
    policy_metadata = policy.metadata      # 获取策略的元数据&lt;/code&gt;&lt;/pre&gt; 如果参数中指定了记录策略行为，则使用 `PolicyRecorder` 包装策略 &lt;pre&gt;&lt;code&gt;    # 记录策略的行为
    if args.record:
        # 使用 PolicyRecorder 记录策略行为
        policy = _policy.PolicyRecorder(policy, &quot;policy_records&quot;)  &lt;/code&gt;&lt;/pre&gt; 接着获取主机名和本地 IP 地址 &lt;pre&gt;&lt;code&gt;    hostname = socket.gethostname()              # 获取主机名
    local_ip = socket.gethostbyname(hostname)    # 获取本地 IP 地址
    logging.info(&quot;Creating server (host: %s, ip: %s)&quot;, hostname, local_ip)  # 记录服务器创建信息&lt;/code&gt;&lt;/pre&gt; 并创建一个 WebSocket 服务器来提供策略服务，最后调用 `serve_forever` 方法启动服务器 &lt;pre&gt;&lt;code&gt;    server = websocket_policy_server.WebsocketPolicyServer(
        policy=policy,
        host=&quot;0.0.0.0&quot;,
        port=args.port,
        metadata=policy_metadata,
    )  # 创建 WebSocket 策略服务器
    server.serve_forever()      # 启动服务器，永远运行&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;在脚本的最后，使用 `logging` 模块配置日志记录，并调用 `main` 函数启动脚本，参数通过 `tyro.cli` 解析&lt;/li&gt;
&lt;/ol&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;更多 还可以看下姚博士所写的这篇文章：openpi π₀ 项目部署运行逻辑（三）——策略推理服务器 serve_policy.py&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;h4 style=&quot;background-color:transparent&quot;&gt;4.2.3(下) 人类下达的任务指令prompt是如何在整个代码库中流转的&lt;/h4&gt; 
&lt;p&gt;&lt;span style=&quot;color:null&quot;&gt;有一朋友在我建的「七月具身：π0复现微调交流群」里提问，&lt;/span&gt;&lt;strong&gt;&lt;span style=&quot;color:#1c7331&quot;&gt;为何不论设置怎样的指令prompt，机器人都执行同一套动作&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;color:null&quot;&gt;「&lt;em&gt;后来，在他们使用多任务数据集训练后，π0可以实现prompt跟随，之前不能的原因是因为&lt;strong&gt;评估时机器人使用了和训练时的不同预备位姿&lt;/strong&gt;&lt;/em&gt;」&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;对此，我特意梳理了下自定义的文本指令prompt在整个π0官方库中的数据流转——花了我一两个小时的时间，^_^&lt;/p&gt; 
&lt;h5&gt;&lt;span style=&quot;color:null&quot;&gt;4.2.3.1 &lt;strong&gt;分别启动WebSocket服务器、WebSocket客户端并互联&lt;/strong&gt;&lt;/span&gt;&lt;/h5&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第一阶段&lt;/strong&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;strong&gt;，设定prompt，随后分别启动WebSocket服务器、WebSocket客户端并互联&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;在上面介绍的这里 设定prompt&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;class Args:
    &quot;&quot;&quot;Arguments for the serve_policy script.&quot;&quot;&quot;

    # Environment to serve the policy for. This is only used when serving default policies.
    env: EnvMode = EnvMode.ALOHA_SIM

    # If provided, will be used in case the &quot;prompt&quot; key is not present in the data, or if the model doesn&#039;t have a default
    # prompt.
    default_prompt: str | None = None&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;&lt;strong&gt;首先，启动策略服务器scripts/serve_policy.py&lt;/strong&gt;&lt;/span&gt;&lt;/u&gt;，在这个策略服务器的代码文件中，main函数中&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第一&lt;/strong&gt;，执行scripts/serve_policy.py中的create_policy&lt;/span&gt; &lt;pre&gt;&lt;code&gt;def main(args: Args) -&amp;gt; None:
    policy = create_policy(args)
    policy_metadata = policy.metadata&lt;/code&gt;&lt;/pre&gt; 而create_policy中，要么调用create_trained_policy，要么调用create_default_policy 
  &lt;hr&gt; &lt;p&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;比如，如果最终选择的是ALOHA的策略，则examples/aloha_real/main.py中的main函数会调用AlohaRealEnvironment类&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;  &lt;p&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;而AlohaRealEnvironment被定义在examples/aloha_real/env.py中的，基于AlohaRealEnvironment的定义，可以看出来其通过其中的『AlohaRealEnvironment-__init__』函数设定环境的初始化「&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;注意，这个AlohaRealEnvironment类中还定义了get_observation，下文会介绍&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;」&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;  &lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第二&lt;/strong&gt;，再执行scripts/serve_policy.py中的&lt;/span&gt; &lt;pre&gt;&lt;code&gt;    policy_metadata = policy.metadata&lt;/code&gt;&lt;/pre&gt; 策略对象的metadata属性会包含default_prompt，且其在policy_metadata = policy.metadata时被提取出来&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第三&lt;/strong&gt;，再在scripts/serve_policy.py中，唤起并初始化WebSocket服务器&lt;/span&gt;  而上面这个&lt;em&gt;&lt;span style=&quot;color:#1c7892&quot;&gt;WebsocketPolicyServer&lt;/span&gt;&lt;/em&gt;，被定义在src/&lt;span style=&quot;color:#be191c&quot;&gt;openpi/serving&lt;/span&gt;/websocket_policy_server.py  于此，上面(&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;scripts/serve_policy.py中的&lt;/em&gt;&lt;/span&gt;)的policy_metadata传递给它(&lt;span style=&quot;color:#be191c&quot;&gt;openpi/serving&lt;/span&gt;中的WebsocketPolicyServer)，&lt;strong&gt;存储在服务器中的&lt;span style=&quot;color:#1c7331&quot;&gt;self._metadata&lt;/span&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第四&lt;/strong&gt;，通过scripts/serve_policy.py中serve_forever的启动WebSocket服务器&lt;/span&gt;  &lt;span style=&quot;color:null&quot;&gt;上面那个serve_forever被定义在src/&lt;/span&gt;&lt;span style=&quot;color:#be191c&quot;&gt;openpi/serving&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;/websocket_policy_server.py中&lt;/span&gt;  &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;u&gt;&lt;strong&gt;&lt;span style=&quot;color:#ed7976&quot;&gt;其次，启动WebSocket客户端：WebsocketClientPolicy&lt;/span&gt;&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;packages/&lt;span style=&quot;color:#ed7976&quot;&gt;openpi-client&lt;/span&gt;/src/openpi_client/websocket_client_policy.py中的WebsocketClientPolicy被初始化时，调用_wait_for_server 连接&lt;span style=&quot;color:#be191c&quot;&gt;WebSocket服务端&lt;/span&gt;  &lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#be191c&quot;&gt;服务端WebsocketPolicyServer&lt;/span&gt;的_handler方法在接受连接后，立即发送&lt;strong&gt;&lt;span style=&quot;color:#1c7331&quot;&gt;self._metadata&lt;/span&gt;&lt;/strong&gt;——&lt;span style=&quot;color:#1a439c&quot;&gt;&lt;em&gt;await websocket.send(packer.pack(self._metadata))&lt;/em&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt; 给客户端&lt;/span&gt;  客户端_wait_for_server的接收到这个元数据之后，便存储在&lt;span style=&quot;color:#511b78&quot;&gt;&lt;strong&gt;_server_metadata&lt;/strong&gt;&lt;/span&gt;中  &lt;/li&gt;
&lt;/ol&gt; 
&lt;h5&gt;&lt;span style=&quot;color:null&quot;&gt;4.2.3.2 客户端发送推理请求、服务端处理推理请求&lt;/span&gt;&lt;/h5&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第二阶段&lt;/strong&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;strong&gt;，客户端发送推理请求、服务端处理推理请求&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;推理请求：客户端向服务端发送全部数据&lt;br&gt;   一方面，环境(examples/aloha_real/env.py)通过get_observation获取观察数据    二方面，客户端的infer(obs)被调用，它将包含状态和图像的obs字典打包发给服务器  &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;服务器处理推理请求&lt;/strong&gt;&lt;br&gt;   首先，服务器的_handler接收到obs字典，然后调用action = self._policy.infer(obs)    其次，策略执行推理&lt;br&gt; 策略内部处理 (policies 下的具体策略文件)——策略的 `infer` 方法被调用以获取prompt&lt;br&gt; 由于传入的 `obs` 字典没有 `&quot;prompt&quot;` 键，策略会查找并使用它在步骤 1 中存储的 `self._default_prompt`，类似prompt_to_use = obs.get(&quot;prompt&quot;, self._default_prompt)`。这里 `prompt_to_use` 会被赋值为自定义的指令字符串&lt;/li&gt;
&lt;/ol&gt; 
&lt;h5&gt;4.2.3.3 模型获得全部输入数据，生成动作序列&lt;/h5&gt; 
&lt;p&gt;&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;第三阶段&lt;/strong&gt;&lt;/span&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;strong&gt;，模型获得全部输入数据，生成动作序列&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;获取到的prompt被传递给分词器Tokennizer，其将文本指令转换为token ID序列&lt;br&gt; 这些token ID序列与图像数据、状态数据一起被输入到π0中&lt;/li&gt;
&lt;li&gt;π0处理这些输入，生成预测的动作序列&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;h4 id=&quot;1.3.4%C2%A0train_test.py%EF%BC%9A%E8%AE%AD%E7%BB%83%E5%92%8C%E6%B5%8B%E8%AF%95%E6%A8%A1%E5%9E%8B&quot; name=&quot;1.3.4%C2%A0train_test.py%EF%BC%9A%E8%AE%AD%E7%BB%83%E5%92%8C%E6%B5%8B%E8%AF%95%E6%A8%A1%E5%9E%8B&quot;&gt;4.2.4 train_test.py：训练和测试模型&lt;/h4&gt; 
&lt;h4 id=&quot;1.3.5%C2%A0train.py%EF%BC%9A%E8%AE%AD%E7%BB%83%E6%A8%A1%E5%9E%8B&quot; name=&quot;1.3.5%C2%A0train.py%EF%BC%9A%E8%AE%AD%E7%BB%83%E6%A8%A1%E5%9E%8B&quot;&gt;4.2.5 train.py：训练模型——损失函数计算、梯度下降、参数更新&lt;/h4&gt; 
&lt;p&gt;这段代码是一个基于JAX的分布式训练脚本，集成了模型初始化、训练循环、日志记录、实验跟踪和检查点管理等功能。以下是对代码的模块化解读：&lt;/p&gt; 
&lt;p&gt;一开始先后涉及日志初始化 (`init_logging`)、Weights &amp;amp; Biases 初始化 (`init_wandb`)、权重加载与验证 (`_load_weights_and_validate`)&lt;/p&gt; 
&lt;p&gt;之后是训练状态初始化 (`init_train_state`)&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;创建优化器（`tx`）和模型实例&lt;/li&gt;
&lt;li&gt;合并预训练参数（若有）到模型状态&lt;/li&gt;
&lt;li&gt;参数类型转换（如冻结参数转`bfloat16`）&lt;/li&gt;
&lt;li&gt;定义分布式分片策略（`fsdp_sharding`）&lt;/li&gt;
&lt;li&gt;返回值：包含模型参数、优化器状态、EMA参数的`TrainState`对象及分片信息&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;再之后，是单步训练`train_step`&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;前向计算：模型计算损失(启用训练模式)，loss_fn中调用的损失函数来自——&lt;span style=&quot;color:#9c8ec1&quot;&gt;&lt;em&gt;1.2.4.3 损失函数***pute_loss：训练模型去噪的准确率(含训练数据集的来源介绍)&lt;/em&gt;&lt;/span&gt; &lt;pre&gt;&lt;code&gt;def train_step(
    config: _config.TrainConfig,
    rng: at.KeyArrayLike,
    state: training_utils.TrainState,
    batch: tuple[_model.Observation, _model.Actions],
) -&amp;gt; tuple[training_utils.TrainState, dict[str, at.Array]]:
    &quot;&quot;&quot;执行单个训练步骤&quot;&quot;&quot;
    # 合并模型定义和参数
    model = nnx.merge(state.model_def, state.params)
    model.train()  # 设置模型为训练模式

    @at.typecheck
    def loss_fn(
        model: _model.BaseModel, rng: at.KeyArrayLike, observation: _model.Observation, actions: _model.Actions
    ):
        &quot;&quot;&quot;损失函数&quot;&quot;&quot;
        # 计算每个数据项的损失
        chunked_loss = model.***pute_loss(rng, observation, actions, train=True)
        return jnp.mean(chunked_loss)  # 返回平均损失&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;随机数生成 &lt;pre&gt;&lt;code&gt;    # 根据当前步数折叠随机数种子，确保每步使用不同随机数
    train_rng = jax.random.fold_in(rng, state.step)

    # 解包批次数据
    observation, actions = batch&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;梯度计算：通过`nnx.value_and_grad`获取梯度，仅更新可训练参数 &lt;pre&gt;&lt;code&gt;    # 过滤出可训练参数
    diff_state = nnx.DiffState(0, config.trainable_filter)

    # 计算损失和梯度
    loss, grads = nnx.value_and_grad(loss_fn, argnums=diff_state)(model, train_rng, observation, actions)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;参数更新：应用优化器更新，合并新参数到模型 &lt;pre&gt;&lt;code&gt;    # 过滤出可训练参数
    params = state.params.filter(config.trainable_filter)

    # 使用优化器更新参数
    updates, new_opt_state = state.tx.update(grads, state.opt_state, params)
    new_params = optax.apply_updates(params, updates)

    # 更新模型参数并返回新的完整状态
    nnx.update(model, new_params)
    new_params = nnx.state(model)&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;EMA维护：指数平滑更新关键参数 &lt;pre&gt;&lt;code&gt;    # 创建新的训练状态，更新步数、参数和优化器状态
    new_state = dataclasses.replace(state, step=state.step + 1, params=new_params, opt_state=new_opt_state)
    if state.ema_decay is not None:
        # 如果使用EMA，更新EMA参数
        new_state = dataclasses.replace(
            new_state,
            ema_params=jax.tree.map(
                lambda old, new: state.ema_decay * old + (1 - state.ema_decay) * new, state.ema_params, new_params
            ),
        )

    # 过滤出核心参数（不包括偏置、缩放等）
    kernel_params = nnx.state(
        model,
        nnx.All(
            nnx.Param,  # 必须是参数
            nnx.Not(nnx_utils.PathRegex(&quot;.*/(bias|scale|pos_embedding|input_embedding)&quot;)),  # 排除特定名称
            lambda _, x: x.value.ndim &amp;gt; 1,  # 必须是多维的
        ),
    )&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;指标收集：损失、梯度范数、参数范数（过滤非核参数） &lt;pre&gt;&lt;code&gt;    # 收集训练信息
    info = {
        &quot;loss&quot;: loss,  # 损失值
        &quot;grad_norm&quot;: optax.global_norm(grads),              # 梯度范数
        &quot;param_norm&quot;: optax.global_norm(kernel_params),     # 参数范数
    }
    return new_state, info&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;最后是主函数`main`&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;环境初始化：日志、JAX配置、随机种子、设备分片&lt;/li&gt;
&lt;li&gt;数据准备：分布式数据加载器，分片策略（数据并行）&lt;/li&gt;
&lt;li&gt;状态恢复：检查点管理器处理恢复逻辑。&lt;/li&gt;
&lt;li&gt;训练循环：&lt;br&gt; JIT编译的分布式训练步骤（`ptrain_step`）&lt;br&gt; 定期日志记录（控制台 + W&amp;amp;B）&lt;br&gt; 检查点保存（间隔保存 + 最终保存）&lt;/li&gt;
&lt;li&gt;清理：等待异步保存操作完成&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;h4 id=&quot;1.3.6%20scripts%2Fdocker&quot; name=&quot;1.3.6%20scripts%2Fdocker&quot;&gt;4.2.6 scripts/docker&lt;/h4&gt; 
&lt;p&gt;好的，下面是对 `openpi-main/scripts/docker` 目录的详细分析。这个目录通包含与 Docker 相关的脚本和配置文件，用于构建和管理 Docker 容器，具体而言，包含以下文件和子目录：&lt;/p&gt; 
 
&lt;p&gt;主要文件和功能如下所示&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;docker/***pose.yml&lt;/li&gt;
&lt;li&gt;docker/install_docker_ubuntu22.sh&lt;/li&gt;
&lt;li&gt;docker/install_nvidia_container_toolkit.sh&lt;/li&gt;
&lt;li&gt;docker/serve_policy.Dockerfile&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;// 待更&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;1.1%20examples%20%EF%BC%9A%E5%90%84%E7%A7%8D%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%B9%B3%E5%8F%B0%E7%9A%84%E7%A4%BA%E4%BE%8B%E5%AE%9E%E7%8E%B0&quot; name=&quot;1.1%20examples%20%EF%BC%9A%E5%90%84%E7%A7%8D%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%B9%B3%E5%8F%B0%E7%9A%84%E7%A4%BA%E4%BE%8B%E5%AE%9E%E7%8E%B0&quot;&gt;第五部分 examples ：各种机器人平台及策略客户端的示例实现&lt;/h2&gt; 
&lt;p&gt;根据π0对应examples模块的结构&lt;/p&gt; 
 
&lt;p&gt;其涉及以下模块&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;aloha_real/：真实机器人ALOHA的示例&lt;/li&gt;
&lt;li&gt;aloha_sim/：ALOHA模拟器的示例&lt;/li&gt;
&lt;li&gt;droid/：DROID机器人的示例&lt;/li&gt;
&lt;li&gt;libero/：LIBERO基准测试的示例&lt;/li&gt;
&lt;li&gt;simple_client/：简单客户端的示例&lt;/li&gt;
&lt;li&gt;ur5/：UR5机器人的示例&lt;/li&gt;
&lt;li&gt;inference.ipynb：推理示例的Jupyter Notebook&lt;/li&gt;
&lt;li&gt;policy_records.ipynb：策略记录示例的Jupyter Notebook&lt;/li&gt;
&lt;/ol&gt; 
&lt;h3&gt;5.1 aloha_real&lt;/h3&gt; 
&lt;p&gt;`aloha_real` 模块是OpenPI项目中用于控制真实ALOHA双臂机器人的完整实现。它提供了从OpenPI策略模型到真实机器人硬件的完整控制链路&lt;/p&gt; 
&lt;h4&gt;5.1.1 核心架构&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;span style=&quot;color:#ed7976&quot;&gt;主控制流程 (main.py)&lt;/span&gt;&lt;br&gt; 作为系统入口点，协调各个组件&lt;br&gt; 其关键组件包括&lt;br&gt;   `WebsocketClientPolicy: 通过WebSocket连接到OpenPI策略服务器&lt;br&gt;   ActionChunkBroker: 处理动作序列，支持25步动作预测&lt;br&gt;   Runtime: 运行时环境，以50Hz频率执行控制循环&lt;br&gt;   PolicyAgent: 策略代理，桥接策略和环境&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#ed7976&quot;&gt;环境接口 (env.py 和 real_env.py)&lt;/span&gt;&lt;br&gt; &lt;strong&gt;`AlohaRealEnvironment` (高级封装)&lt;/strong&gt;：提供标准化的环境接口、处理图像预处理和尺寸调整 (224x224)、将图像格式从 HWC 转换为 CHW&lt;br&gt; &lt;br&gt; &lt;strong&gt;`RealEnv` (底层硬件接口)&lt;/strong&gt;&lt;br&gt;   双臂控制: 左右两个Interbotix vx300s机械臂&lt;br&gt;   动作空间 (14维)： &lt;pre&gt;&lt;code&gt;  [left_arm_qpos(6), left_gripper(1), right_arm_qpos(6), right_gripper(1)]&lt;/code&gt;&lt;/pre&gt;   观察空间:&lt;br&gt; &lt;span style=&quot;color:#79c6cd&quot;&gt;`qpos`: 关节位置 (14维)&lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#6eaad7&quot;&gt;`qvel`: 关节速度 (14维) &lt;/span&gt;&lt;br&gt; &lt;span style=&quot;color:#511b78&quot;&gt;`images`: 4个摄像头视角&lt;/span&gt;&lt;br&gt; &lt;em&gt;&lt;span style=&quot;color:#9c8ec1&quot;&gt;- `cam_high`: 俯视视角&lt;br&gt; - `cam_low`: 平视视角&lt;br&gt; - `cam_left_wrist`: 左手腕视角&lt;br&gt; - `cam_right_wrist`: 右手腕视角&lt;/span&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#ed7976&quot;&gt;硬件常量定义 (constants.py)&lt;/span&gt;&lt;br&gt; 关节名称: `[&quot;waist&quot;, &quot;shoulder&quot;, &quot;elbow&quot;, &quot;forearm_roll&quot;, &quot;wrist_angle&quot;, &quot;wrist_rotate&quot;]`&lt;br&gt; 夹爪位置限制: 开合状态的物理限位&lt;br&gt; 标准化函数: 将夹爪位置映射到[0,1]区间&lt;br&gt; 默认复位姿态: `[0, -0.96, 1.16, 0, -0.3, 0]`&lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#ed7976&quot;&gt;数据转换工具&lt;/span&gt;&lt;br&gt; &lt;strong&gt;convert_aloha_data_to_lerobot.py&lt;/strong&gt;&lt;br&gt;   将ALOHA原生数据格式转换为LeRobot标准格式&lt;br&gt;   支持训练数据的预处理和标准化&lt;br&gt; &lt;br&gt; &lt;strong&gt;robot_utils.py&lt;/strong&gt;&lt;br&gt; 包含机器人设置和数据记录工具&lt;br&gt; Recorder: 记录关节状态数据&lt;br&gt; ImageRecorder: 记录摄像头图像数据&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;5.1.2 系统工作流程与部署方式&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color:null&quot;&gt;初始化阶段&lt;br&gt; 启动ROS节点 → 初始化双臂机器人 → 连接摄像头 → 建立WebSocket连接&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:null&quot;&gt;运行时循环(50Hz)&lt;br&gt; 获取观察(图像+状态) → 发送到策略服务器 → 接收动作序列 → 执行动作 → 更新状态&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:null&quot;&gt;动作执行&lt;br&gt; 策略预测25步动作序列&lt;br&gt; `ActionChunkBroker`管理动作缓冲和执行&lt;br&gt; 每步动作包含14维关节目标位置&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span style=&quot;color:null&quot;&gt;&lt;u&gt;至于部署方式有以下两种&lt;/u&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;一个是Docker部署&lt;/span&gt;，则直接安装 Docker 并运行&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;export SERVER_ARGS=&quot;--env ALOHA --default_prompt=&#039;take the toast out of the toaster&#039;&quot;
docker ***pose -f examples/aloha_real/***pose.yml up --build&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span style=&quot;color:#fe2c24&quot;&gt;一个是本地部署，其需要启动3个终端&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;[终端1] 机器人客户端 ←→ WebSocket ←→ [终端3] 策略服务器/serve_policy.py
    ↓                                      ↑
[终端2] ROS硬件层                      OpenPI模型推理&lt;/code&gt;&lt;/pre&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;span style=&quot;color:#1c7331&quot;&gt;&lt;strong&gt;机器人控制客户端&lt;/strong&gt;&lt;/span&gt;，&lt;em&gt;相当于WebSocket客户端&lt;/em&gt;&lt;br&gt; 该客户端从机器人硬件获取观察数据(图像 + 状态)，然后通过WebSocket发送观察数据到&lt;span style=&quot;color:#be191c&quot;&gt;策略服务器&lt;/span&gt;&lt;br&gt; 之后，接收&lt;span style=&quot;color:#be191c&quot;&gt;策略服务器&lt;/span&gt;返回的动作指令，将动作指令发送给机器人执行&lt;br&gt; ————————————————&lt;br&gt; 具体而言，初始化虚拟环境并运行机器人控制主程序 &lt;pre&gt;&lt;code&gt;# Create virtual environment
uv venv --python 3.10 examples/aloha_real/.venv
source examples/aloha_real/.venv/bin/activate
uv pip sync examples/aloha_real/requirements.txt
uv pip install -e packages/openpi-client
 
# Run the robot
python -m examples.aloha_real.main&lt;/code&gt;&lt;/pre&gt; 以上代码分别对应&lt;br&gt; &lt;strong&gt;1 创建Python 3.10虚拟环境&lt;/strong&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;使用 uv 工具创建一个 Python 虚拟环境，路径为 examples/aloha_real/.venv  &lt;br&gt; uv 是一个替代 venv + pip 的高性能依赖管理工具&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;strong&gt;2 激活虚拟环境&lt;/strong&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;进入刚才创建的虚拟环境，使之后的 Python 执行与 pip 安装都仅作用于该环境&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;strong&gt;3 安装所需依赖&lt;/strong&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;安装 requirements.txt 中精确指定的依赖版本&lt;br&gt; sync 是比 install -r 更稳定的方式，确保包版本锁定且无冗余&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;strong&gt;4 安装 openpi-client 为本地开发模式&lt;/strong&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;使用 -e（editable）方式安装本地 openpi-client 包，允许实时修改代码而无需重装&lt;/em&gt;&lt;/span&gt;&lt;br&gt; &lt;strong&gt;5 启动主程序控制机器人（主线程）&lt;/strong&gt;&lt;br&gt; &lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;启动 Pi0 控制机器人动作的主循环，包括摄像头读取、电机控制、感知更新等&lt;br&gt; 此模块会连接 ROS 节点并与推理服务通信&lt;/em&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;span style=&quot;color:#1a439c&quot;&gt;ROS节点服务&lt;/span&gt;&lt;/strong&gt;&lt;br&gt; 其作用为：启动机器人硬件驱动、启动摄像头节点、&lt;strong&gt;提供底层硬件接口&lt;/strong&gt;&lt;br&gt; ————————————————&lt;br&gt; 具体而言，启动ROS驱动 &lt;pre&gt;&lt;code&gt;roslaunch aloha ros_nodes.launch&lt;/code&gt;&lt;/pre&gt; 使用 ROS 启动 aloha 平台硬件驱动，包括：&lt;br&gt; 控制机械臂的 Dynamixel 电机驱动&lt;br&gt; 相机接口节点&lt;br&gt; ROS 的 topic 广播、TF 树等&lt;br&gt; 这一步是让机器人硬件对接 ROS 网络层，确保后续主控程序可调用硬件资源 &lt;p&gt;&lt;span style=&quot;color:#7b7f82&quot;&gt;&lt;em&gt;PS：如姚博士所说，此处 ROS 配置根据项目子模块配置，以及 ROS 系统主要针对 ALOHA 一类的舵机机器人&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;  &lt;/li&gt;
&lt;li&gt;
&lt;span style=&quot;color:#be191c&quot;&gt;&lt;strong&gt;openpi策略服务器&lt;/strong&gt;&lt;/span&gt;，&lt;em&gt;相当于WebSocket服务器&lt;/em&gt;&lt;br&gt; 其作用为：加载训练好的openpi模型，监听WebSocket连接，以及接收观察数据 运行策略推理，从而最终返回动作序列&lt;br&gt; ————————————————&lt;br&gt; 具体而言，运行策略推理服务器 &lt;pre&gt;&lt;code&gt;uv run scripts/serve_policy.py --env ALOHA --default_prompt=&#039;take the toast out of the toaster&#039;&lt;/code&gt;&lt;/pre&gt; 上述代码相当于启动 serve_policy.py 推理服务：&lt;br&gt; 加载一个策略（如 pi0）和预训练权重&lt;br&gt; 等待来自主控制进程的请求（语言提示 + 视觉输入）&lt;br&gt; 返回动作序列给控制主进程&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;综上，三进程间的协同流程可以总结为：&lt;/p&gt; 
&lt;pre&gt;&lt;code&gt;[ROS系统（终端2）] &amp;lt;== 硬件数据 ==&amp;gt; [主控进程 main.py（终端1）] &amp;lt;== 请求 ==&amp;gt; [推理服务 serve_policy.py（终端3）]&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;即三个终端分别主要负责：&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;启动虚拟环境 + 控制主逻辑，控制主程序需要同步感知与动作控制&lt;/li&gt;
&lt;li&gt;启动 ROS 节点驱动机器人硬件，ROS 启动通常是独立的进程&lt;/li&gt;
&lt;li&gt;启动语言策略模型的推理服务，推理服务需常驻监听 socket 请求&lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;// 待更&lt;/p&gt;</description><pubDate>Wed, 26 Nov 2025 17:21:59 +0800</pubDate></item><item><title>RabbitMQ</title><link>https://ajaa.cn/869.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172159176414891953959.jpg&quot; alt=&quot;RabbitMQ&quot; title=&quot;RabbitMQ&quot; /&gt;&lt;/p&gt;&lt;p&gt;在消息队列（MQ）中，确保消息成功传递是一个关键问题。消息传递的过程通常包括以下几个阶段：publisher（生产者） -&amp;gt; exchange（交换机） -&amp;gt; queue（队列） -&amp;gt; consumer（消费者）。为了确保消息在每个阶段都能成功传递，我们需要采取一系列措施来保证消息的可靠性。&lt;/p&gt;

&lt;h2&gt;生产者的可靠性&lt;/h2&gt;

&lt;h3&gt;重试机制&lt;/h3&gt;

&lt;p&gt;当生产者与交换机（或队列，如果没有交换机）之间的连接不稳定时，生产者发送的消息可能会在传输过程中丢失。在这种情况下，生产者需要等待一段时间以获取响应。如果未收到响应，生产者应尝试重新发送消息。重试次数应有限制，以防止因持续重试而占用过多资源。此外，重试之间应有一定的间隔时间，以避免频繁重试导致资源浪费。&lt;/p&gt;

&lt;p&gt;由于发送消息时会占用通道，其他业务操作可能会被阻塞，直到消息发送完成（无论成功或失败）。因此，对发送消息的重试机制进行限制是必要的，以防止因连接问题导致资源被长时间占用。&lt;/p&gt;

&lt;p&gt;以下是一个在Spring Boot中配置生产者重试机制的示例：&lt;/p&gt;

&lt;blockquote&gt;
&lt;pre&gt;
spring:
  rabbitmq:
    connection-timeout: 1s  # 连接超时时间
    template:
      retry:
        enabled: true  # 开启重试机制
        initial-interval: 1000ms  # 初始重试间隔时间
        multiplier: 1  # 重试间隔时间的倍数
        max-attempts: 5  # 最大重试次数&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;在这个配置中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;&lt;code&gt;connection-timeout&lt;/code&gt; 设置了连接超时的时间。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;code&gt;enabled: true&lt;/code&gt; 开启了重试机制。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;code&gt;initial-interval: 1000ms&lt;/code&gt; 设置了在连接失败后，首次重试前的等待时间。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;code&gt;multiplier: 1&lt;/code&gt; 设置了每次重试后等待时间的倍数（在此例中，等待时间保持不变）。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;code&gt;max-attempts: 5&lt;/code&gt; 设置了最大重试次数，超过该次数后将不再重试。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过这种配置，生产者在发送消息失败后会自动进行重试，直到达到最大重试次数或消息成功发送。这种机制有效地提高了消息传递的可靠性，同时避免了因持续重试而导致的资源浪费&lt;/p&gt;

&lt;p&gt;根据您提供的信息，我们可以分析MQ连接失败时的重试行为。以下是详细的分析：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;连接超时时间&lt;/strong&gt;：设置为1秒。这意味着如果MQ在1秒内未能成功连接，连接尝试将被视为失败。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;初始重试间隔&lt;/strong&gt;：设置为1秒。在第一次连接失败后，系统会等待1秒再进行下一次连接尝试。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;等待时间倍数&lt;/strong&gt;：设置为1。这意味着每次重试的等待时间保持不变。因此，每次重试的间隔时间为1秒（等待时间） + 1秒（连接超时时间） = 2秒。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;最大重试次数&lt;/strong&gt;：设置为5次。系统会在达到最大重试次数后停止尝试连接。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;根据这些设置，系统在连接失败后的行为如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;第一次连接失败后，等待1秒，然后进行第二次连接尝试。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;第二次连接失败后，再次等待1秒，然后进行第三次连接尝试。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;第三次连接失败后，系统将停止尝试连接。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果&lt;code&gt;multiplier&lt;/code&gt;设置为2，重试行为将有所不同：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;第一次连接失败后，等待1秒，然后进行第二次连接尝试。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;第二次连接失败后，等待时间翻倍为2秒，然后进行第三次连接尝试。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;第三次连接失败后，等待时间再次翻倍为4秒，然后系统将停止尝试连接。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这种配置确保了系统在连接失败时能够进行有限次数的重试，同时通过调整等待时间倍数来控制重试的频率，以避免过度占用资源。&lt;/p&gt;

&lt;h3&gt;确认机制&lt;/h3&gt;

&lt;p&gt;在正常情况下，消息传递到MQ后不会发生丢失，但我们仍需对消息丢失有所防备。为了及时发现消息丢失，MQ通常使用&lt;strong&gt;Publisher Confirm&lt;/strong&gt;和&lt;strong&gt;Publisher Return&lt;/strong&gt;两种机制来进行预警。&lt;/p&gt;

&lt;hr&gt;
&lt;h5&gt;&lt;strong&gt;消息丢失的可能情况&lt;/strong&gt;&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;MQ内部故障&lt;/strong&gt;：MQ服务器内部出现问题，导致消息丢失。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;交换机或队列不存在&lt;/strong&gt;：消息无法找到目标交换机或队列。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;RoutingKey不匹配&lt;/strong&gt;：消息的路由键（RoutingKey）没有匹配的队列。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;其他异常情况&lt;/strong&gt;：例如网络故障、消息过期（TTL）等。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h5&gt;&lt;strong&gt;消息传递的几种状态&lt;/strong&gt;&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;路由失败，但投递成功&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;消息传递到MQ后，MQ服务器成功接收并存储了消息（投递成功）。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;但由于路由失败（例如RoutingKey不匹配或目标队列不存在），消息无法被正确路由到下一个节点（例如队列）。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;MQ服务器会返回&lt;strong&gt;ACK&lt;/strong&gt;确认投递成功，同时通过&lt;strong&gt;Publisher Return&lt;/strong&gt;机制返回路由失败的异常信息。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;临时消息投递成功&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;临时消息传递到MQ后，被存储在内存中。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;MQ服务器返回&lt;strong&gt;ACK&lt;/strong&gt;，表示消息已成功投递。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;持久化消息投递成功&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;持久化消息传递到MQ后，被持久化存储到磁盘。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;MQ服务器返回&lt;strong&gt;ACK&lt;/strong&gt;，表示消息已成功投递。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;投递失败&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;&lt;li&gt;
		&lt;p&gt;如果消息未能成功传递到MQ（例如网络故障或MQ服务器不可用），MQ服务器会返回&lt;strong&gt;NACK&lt;/strong&gt;，表示投递失败。&lt;/p&gt;
		&lt;/li&gt;&lt;/ul&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h5&gt;&lt;strong&gt;Publisher Confirm 和 Publisher Return 的作用&lt;/strong&gt;&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;Publisher Confirm&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;通过返回&lt;strong&gt;ACK&lt;/strong&gt;或&lt;strong&gt;NACK&lt;/strong&gt;，告知生产者消息是否成功投递到MQ服务器。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;&lt;strong&gt;ACK&lt;/strong&gt;表示消息已成功投递（无论是临时消息还是持久化消息）。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;&lt;strong&gt;NACK&lt;/strong&gt;表示消息投递失败。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;Publisher Return&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;当消息成功投递到MQ服务器但路由失败时，通过Publisher Return返回异常信息。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;帮助生产者及时发现消息无法被正确路由的问题。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h5&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;投递成功&lt;/strong&gt;：消息成功到达MQ服务器并被存储（临时消息存储在内存中，持久化消息存储在磁盘中），MQ返回&lt;strong&gt;ACK&lt;/strong&gt;。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;路由失败&lt;/strong&gt;：消息成功投递到MQ服务器，但无法被正确路由到目标队列，MQ通过&lt;strong&gt;Publisher Return&lt;/strong&gt;返回异常信息。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;投递失败&lt;/strong&gt;：消息未能成功传递到MQ服务器，MQ返回&lt;strong&gt;NACK&lt;/strong&gt;。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过&lt;strong&gt;Publisher Confirm&lt;/strong&gt;和&lt;strong&gt;Publisher Return&lt;/strong&gt;机制，生产者可以及时了解消息的投递状态，从而有效预防和发现消息丢失问题。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;spring:&lt;br&gt;
  rabbitmq:&lt;br&gt;
    publisher-confirm-type: correlated &lt;br&gt;
    publisher-returns: true &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;默认情况下, Publisher Confirm和Publisher Return两个机制是关闭的. publisher-confirm-type有三钟状态&lt;/p&gt;

&lt;p&gt;none: 默认关闭, 就是这个状态&lt;/p&gt;

&lt;p&gt;simple: 同步进行, 需要等到回复状态之后才会继续业务&lt;/p&gt;

&lt;p&gt;correlated: 异步进行, 在等待回复状态的同时, 业务可以继续进行处理&lt;/p&gt;

&lt;h2&gt;MQ的可靠性&lt;/h2&gt;

&lt;h3&gt;数据持久化&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;确保消息队列（MQ）可靠性的关键措施&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在分布式系统中，消息队列（MQ）是异步通信的核心组件。然而，即使在正常情况下，消息在到达MQ后仍有可能丢失。因此，确保MQ的可靠性至关重要。本文将介绍几种常见的MQ可靠性方案。&lt;/p&gt;

&lt;h4&gt;1. 消息丢失的原因&lt;/h4&gt;

&lt;p&gt;MQ通常将消息存储在内存中进行处理和传递，这种方式虽然高效，但在MQ服务重启或崩溃时，内存中的消息会丢失。为了解决这一问题，我们需要采取以下措施来增强MQ的可靠性。&lt;/p&gt;

&lt;h4&gt;2. MQ可靠性方案&lt;/h4&gt;

&lt;h5&gt;2.1 交换机的持久化&lt;/h5&gt;

&lt;p&gt;交换机（Exchange）是MQ中路由消息的关键组件。通过将交换机设置为持久化，可以确保在MQ重启后，交换机的配置和元数据不会丢失，从而保证消息能够继续被正确路由。&lt;/p&gt;

&lt;h5&gt;2.2 队列的持久化&lt;/h5&gt;

&lt;p&gt;队列（Queue）是消息的存储载体。将队列设置为持久化后，即使MQ服务重启，队列中的消息也不会丢失。持久化队列会将消息存储到磁盘中，而不是仅仅依赖内存。&lt;/p&gt;

&lt;h5&gt;2.3 消息的持久化&lt;/h5&gt;

&lt;p&gt;除了交换机和队列的持久化，消息本身也需要进行持久化处理。持久化的消息在未被消费前会一直存储在磁盘中，只有在被成功消费后才会被删除。这种方式可以有效避免因MQ重启或崩溃导致的消息丢失。&lt;/p&gt;

&lt;h4&gt;3. 生产者确认机制&lt;/h4&gt;

&lt;p&gt;为了进一步增强MQ的可靠性，可以启用生产者确认机制（Publisher Confirms）。当消息被持久化存储到磁盘后，MQ会向生产者发送一个确认（ACK），告知消息已安全存储。这种机制可以确保消息不会在传输过程中丢失。&lt;/p&gt;

&lt;h4&gt;4. 批量持久化与异步处理&lt;/h4&gt;

&lt;p&gt;为了提高性能，MQ通常不会逐条持久化消息，而是采用批量持久化的方式。这种方式可以显著减少磁盘I/O操作，提升系统的整体效率。同时，推荐使用异步方式进行持久化，以避免阻塞消息的处理流程。&lt;/p&gt;

&lt;h4&gt;5. 总结&lt;/h4&gt;

&lt;p&gt;通过交换机的持久化、队列的持久化、消息的持久化以及生产者确认机制，可以显著提升MQ的可靠性。此外，批量持久化和异步处理能够在不牺牲可靠性的前提下，进一步提高系统的性能。在实际应用中，建议根据业务需求合理配置这些机制，以确保消息的可靠传递。&lt;/p&gt;

&lt;h3&gt;LazyQueue&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;消息存储在内存中的优势与挑战&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RabbitMQ 默认将消息存储在内存中，因为内存的读写速度远高于硬盘，这可以显著提高消息处理的效率。然而，这种设计也带来了一些潜在的问题，尤其是在消息量激增的情况下。&lt;/p&gt;

&lt;h4&gt;1. 内存存储的优势&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;高效读写：&lt;/strong&gt; 内存的访问速度比硬盘快得多，因此将消息存储在内存中可以大幅降低消息收发的延迟，提升系统性能。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;低延迟：&lt;/strong&gt; 对于实时性要求较高的场景，内存存储能够确保消息快速传递。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;2. 内存存储的挑战&lt;/h4&gt;

&lt;p&gt;尽管内存存储有诸多优势，但在某些情况下，可能会面临以下问题：&lt;/p&gt;

&lt;h5&gt;2.1 生产者消息激增&lt;/h5&gt;

&lt;p&gt;当生产者的消息发送速率突然增加时，可能会导致消息在内存中大量堆积。&lt;/p&gt;

&lt;h5&gt;2.2 消费者处理能力不足&lt;/h5&gt;

&lt;p&gt;如果消费者的处理速度跟不上生产者的发送速度，消息会在内存中积压，占用大量内存资源。&lt;/p&gt;

&lt;h5&gt;2.3 内存限制与 PageOut&lt;/h5&gt;

&lt;p&gt;内存的容量是有限的，当消息积压超过内存的极限时，RabbitMQ 会将部分消息从内存转移到硬盘中，这个过程称为 &lt;strong&gt;PageOut&lt;/strong&gt;。在 PageOut 过程中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;生产者可能会被拒绝发送消息（流控机制生效）。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;消费者也无法消费消息，因为部分消息正在从内存转移到硬盘。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;内存资源会被占用，影响系统的整体性能。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;3. Lazy Queue 的引入&lt;/h4&gt;

&lt;p&gt;为了解决上述问题，RabbitMQ 在 &lt;strong&gt;3.12 版本&lt;/strong&gt; 之后引入了 &lt;strong&gt;Lazy Queue&lt;/strong&gt; 机制。Lazy Queue 的核心设计思想是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;消息优先存储到硬盘：&lt;/strong&gt; 消息不会直接存储在内存中，而是批量写入硬盘。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;懒加载机制：&lt;/strong&gt; 只有当消费者需要消费消息时，才会将消息从硬盘加载到内存中。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;支持海量消息存储：&lt;/strong&gt; Lazy Queue 可以轻松处理百万级甚至更多的消息量，而不会对内存造成过大压力。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;4. Lazy Queue 的优势&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;降低内存压力：&lt;/strong&gt; 消息主要存储在硬盘中，内存占用大幅减少。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;提高系统稳定性：&lt;/strong&gt; 即使消息量激增，也不会因为内存不足而导致消息丢失或系统崩溃。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;适合高吞吐量场景：&lt;/strong&gt; 对于消息量大但实时性要求不高的场景，Lazy Queue 是一个理想的选择。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;消费者的可靠性&lt;/h2&gt;

&lt;p&gt;当RabbitMq将消息传递给消费者, 依旧会存在之前的消息丢失, 比如消费者处理异常, 发送失败, 消费者宕机等情况&lt;/p&gt;

&lt;h4&gt;消费者确认机制&lt;/h4&gt;

&lt;p&gt;当RabbitMQ将消息传递给消费者时，仍然可能存在消息丢失的情况，例如消费者处理异常、发送失败、消费者宕机等。为了确保消息的可靠传递，RabbitMQ提供了消费者确认机制，允许消费者在处理消息后向RabbitMQ反馈消息的处理状态。&lt;/p&gt;

&lt;h4&gt;消费者确认机制&lt;/h4&gt;

&lt;p&gt;RabbitMQ的消费者确认机制主要分为以下三种状态：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;ack（确认）&lt;/strong&gt;：消息处理成功，RabbitMQ会将该消息从队列中删除。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;nack（否定确认）&lt;/strong&gt;：消息处理异常，RabbitMQ会将消息重新加载回队列，进行重试。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;reject（拒绝）&lt;/strong&gt;：消息处理异常，且消息无法被处理，RabbitMQ会直接删除该消息。&lt;code&gt;reject&lt;/code&gt;通常用于消息类型不匹配或无法处理的场景。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Spring AMQP中的确认模式&lt;/h4&gt;

&lt;p&gt;Spring AMQP为开发者提供了三种确认模式，以简化消息确认的处理：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;none模式&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;当消息发送到消费者后，RabbitMQ会立即确认（ack）并删除消息，无论消费者是否成功处理。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;适用于对消息丢失不敏感的场景。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;manual模式&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;开发者需要手动调用&lt;code&gt;ack&lt;/code&gt;、&lt;code&gt;nack&lt;/code&gt;或&lt;code&gt;reject&lt;/code&gt;来确认消息的处理状态。&lt;/p&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;适用于需要精细控制消息确认的场景，但可能会造成业务代码的污染。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;auto模式&lt;/strong&gt;：&lt;/p&gt;

	&lt;ul&gt;
&lt;li&gt;
		&lt;p&gt;Spring AMQP会自动根据消息处理的结果发送确认。&lt;/p&gt;

		&lt;ul&gt;
&lt;li&gt;
			&lt;p&gt;如果业务正常处理，返回&lt;code&gt;ack&lt;/code&gt;。&lt;/p&gt;
			&lt;/li&gt;
&lt;li&gt;
			&lt;p&gt;如果发生业务异常，返回&lt;code&gt;nack&lt;/code&gt;，消息会重新入队进行重试。&lt;/p&gt;
			&lt;/li&gt;
&lt;li&gt;
			&lt;p&gt;如果发生消息校验或处理异常，返回&lt;code&gt;reject&lt;/code&gt;，消息会被直接删除。&lt;/p&gt;
			&lt;/li&gt;
&lt;/ul&gt;
		&lt;/li&gt;
&lt;li&gt;
		&lt;p&gt;适用于大多数常见的业务场景。&lt;/p&gt;
		&lt;/li&gt;
&lt;/ul&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;配置示例&lt;/h4&gt;

&lt;p&gt;在Spring Boot中，可以通过以下配置来设置确认模式：&lt;/p&gt;

&lt;blockquote&gt;
&lt;pre&gt;
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto # 可选值为 none, manual, auto&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;h4&gt;异常处理的最佳实践&lt;/h4&gt;

&lt;p&gt;在&lt;code&gt;auto&lt;/code&gt;模式下，建议根据异常类型决定是返回&lt;code&gt;nack&lt;/code&gt;还是&lt;code&gt;reject&lt;/code&gt;。例如，对于可重试的业务异常（如网络抖动），可以返回&lt;code&gt;nack&lt;/code&gt;；对于不可恢复的异常（如消息格式错误），可以返回&lt;code&gt;reject&lt;/code&gt;。&lt;/p&gt;

&lt;h3&gt;失败重试机制&lt;/h3&gt;

&lt;h4&gt;业务异常处理与重试机制&lt;/h4&gt;

&lt;p&gt;在消息队列中，如果不对业务异常处理进行合理的限制，每次业务发生异常时，消息会重新入队并进行重试。如果没有适当的控制机制，消息可能会不断重试，导致系统资源被空耗，甚至引发消息堆积、系统崩溃等问题。&lt;/p&gt;

&lt;p&gt;在Spring AMQP中，可以通过配置RabbitMQ的监听器重试机制来避免这种情况。以下是一个典型的配置示例：&lt;/p&gt;

&lt;blockquote&gt;
&lt;pre&gt;
spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true          # 开启重试机制
          max-attempts: 5         # 最大重试次数
          initial-interval: 1000  # 初始重试间隔时间（毫秒）
          multiplier: 1           # 重试间隔时间的倍数
          stateless: true         # 是否启用无状态重试&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;h5&gt;配置项说明：&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;enabled&lt;/strong&gt;: 是否启用重试机制。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;max-attempts&lt;/strong&gt;: 最大重试次数。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;initial-interval&lt;/strong&gt;: 初始重试间隔时间。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;multiplier&lt;/strong&gt;: 重试间隔时间的倍数。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;stateless&lt;/strong&gt;: 是否启用无状态重试。设置为&lt;code&gt;true&lt;/code&gt;时，表示每次重试都是独立的，不会保留前一次重试的状态。这对于一些涉及上下文代码或变量传递的业务场景非常重要。如果业务逻辑依赖于某些变量的初始状态，启用无状态重试可以确保每次重试时变量都恢复到初始值，避免前一次重试的结果影响后续业务。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;h5&gt;无状态重试的重要性&lt;/h5&gt;

&lt;p&gt;在某些业务场景中，业务逻辑可能会依赖于某些变量的初始状态。例如：&lt;/p&gt;

&lt;pre&gt;
&lt;code class=&quot;language-java&quot;&gt;int x = 5;  // 初始值为5
// 业务逻辑处理
x = 8;      // 业务处理后，x的值变为8&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果在下一次重试时，变量&lt;code&gt;x&lt;/code&gt;仍然保留上一次处理后的值（即8），而不是恢复到初始值（5），这可能会导致业务逻辑出现错误。通过设置&lt;code&gt;stateless: true&lt;/code&gt;，可以确保每次重试时，业务逻辑中的变量都会恢复到初始状态，从而避免这种问题。&lt;/p&gt;

&lt;h3&gt;失败处理策略&lt;/h3&gt;

&lt;p&gt;当消息在本地多次重试失败后, 超过重试次数的限制, 会被队列删除, 但是这对于消息可靠性要求高的业务并不友好, 所以提供了一个MessageRecovery接口, 这个接口较好的实现是RepublishMessageRecovery类, 它是把异常信息放到一个单独的队列中, 后续人工介入处理&lt;/p&gt;

&lt;h3&gt;业务幂等性&lt;/h3&gt;

&lt;p&gt;在执行某个业务操作时，无论该操作被执行一次还是多次，最终的业务结果都是一致的，这就是&lt;strong&gt;幂等性&lt;/strong&gt;。幂等性在分布式系统和网络通信中尤为重要，因为它可以有效避免重复操作带来的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;举个例子&lt;/strong&gt;：当你在购物时进行扣款操作，如果由于网络延迟或其他原因，扣款请求被重复发送了两次，而你被扣了两次钱，这显然是不可接受的。为了避免这种情况，我们需要确保扣款操作的幂等性。&lt;/p&gt;

&lt;h4&gt;唯一消息ID&lt;/h4&gt;

&lt;p&gt;在消息队列系统中，为了确保消息的幂等性（即多次处理同一消息不会产生重复的效果），通常会在发送消息时附带一个唯一ID。这个唯一ID可以是全局唯一标识符（UUID）或根据业务规则生成的唯一值。消费者在接收到消息后，首先会检查该ID是否已经存在于数据库中。如果该ID已经存在，说明该消息已经被处理过，消费者会直接跳过该消息，避免重复处理；如果该ID不存在，消费者则会处理该消息，并将该ID存储到数据库中，以确保后续重复消息不会被重复处理。&lt;/p&gt;

&lt;h4&gt;业务判断&lt;/h4&gt;

&lt;p&gt;除了使用唯一消息ID来确保消息的幂等性外，业务判断也是处理重复消息或请求的重要手段。业务判断是通过业务本身的逻辑来进行判断，确保即使消息ID不同，但业务内容相同的请求也不会被重复处理。例如，在订单系统中，可以通过订单号、用户ID等业务字段来判断是否已经处理过相同的请求。&lt;/p&gt;

&lt;p&gt;通过结合唯一消息ID和业务判断，可以有效地确保消息队列系统中的消息处理是幂等的，从而避免重复处理带来的业务问题。&lt;/p&gt;

&lt;h3&gt;兜底方案&lt;/h3&gt;

&lt;p&gt;尽管MQ消息系统已经尽可能减少了消息丢失的可能性，但在实际应用中，消息丢失的情况仍然可能发生。因此，我们需要主动采取措施来确保业务结果的准确性。具体来说，可以通过定时任务来实现这一目标。定时任务会每隔一段时间对业务结果进行比对和检查，确保数据的完整性和一致性。&lt;/p&gt;

&lt;p&gt;这种兜底方案的核心在于通过定期检查来弥补消息丢失可能带来的影响，从而保证业务的最终一致性。&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;strong&gt;消息幂等与性能优化&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;在高数据量的场景下，消息幂等性不仅要保证消息处理的正确性，还需要提升消息的存取速度和系统效率。以下是几种常见的优化方案：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;集群部署&lt;/strong&gt;：通过集群化部署，可以提高系统的稳定性和吞吐量，分散单节点的压力，从而提升整体性能。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;分库分表&lt;/strong&gt;：对于数据量巨大的场景，可以采用分库分表的方式，将数据分散到多个数据库或表中，减少单表的数据量，提升查询和写入效率。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;数据生命周期管理&lt;/strong&gt;：通过定时任务对数据进行归档、移动或删除，确保数据的时效性，避免无效数据占用存储资源，从而提升系统性能。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些方法结合使用，可以在保证消息幂等性的同时，有效提升系统的处理能力和效率&lt;/p&gt;

&lt;h2&gt;延时消息&lt;/h2&gt;

&lt;p&gt;在网上售卖货物时，由于商品数量有限，当用户下单后，数据库会自动扣减商品库存。然而，如果用户未在规定时间内完成付款，这些商品就会被该用户占用，导致其他购物者无法购买。为了解决这一问题，我们需要对未付款的用户设置时间限制，要求他们在规定时间内完成付款，否则库存将被释放。&lt;/p&gt;

&lt;p&gt;这种需要在一定时间后再执行的任务被称为&lt;strong&gt;延时任务&lt;/strong&gt;。消息队列（MQ）提供了两种处理延时任务的解决方案：&lt;strong&gt;死信交换机&lt;/strong&gt;和&lt;strong&gt;延时消息插件&lt;/strong&gt;。&lt;/p&gt;

&lt;h3&gt;死信交换机&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;死信&lt;/strong&gt;是指以下几种情况下的消息：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;超时未被处理的消息&lt;/strong&gt;：消息在队列中等待时间过长，未被消费者处理。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;队列满员&lt;/strong&gt;：当队列达到最大容量时，新消息无法进入，成为死信。&lt;/p&gt;
	&lt;/li&gt;
&lt;li&gt;
	&lt;p&gt;&lt;strong&gt;消费失败的消息&lt;/strong&gt;：消费者在处理消息时返回&lt;code&gt;nack&lt;/code&gt;或&lt;code&gt;reject&lt;/code&gt;，并且将&lt;code&gt;requeue&lt;/code&gt;设置为&lt;code&gt;false&lt;/code&gt;，表示消息消费失败且不再重新入队。&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;当一个队列中存在死信消息时，可以通过配置&lt;code&gt;dead-letter-exchange&lt;/code&gt;参数，将这些死信消息转发到一个特定的交换机，这个交换机被称为&lt;strong&gt;死信交换机&lt;/strong&gt;。死信交换机会与一个或多个队列绑定，用于接收和处理这些死信消息。&lt;/p&gt;

&lt;h3 style=&quot;background-color:transparent&quot;&gt;延时消息插件&lt;/h3&gt;

&lt;p&gt;延时消息插件是一种简化消息延时处理的工具，相较于使用死信交换机的方案，它减少了实现延时消息所需的步骤。延时消息插件在基本的消息流程（生产者 -&amp;gt; 交换机 -&amp;gt; 队列 -&amp;gt; 消费者）中，通过将交换机设计为具备延时和暂时存储消息的能力，从而直接实现消息的延时投递。这种方式避免了传统死信交换机方案中需要额外设置死信队列、绑定死信交换机等复杂操作，简化了系统的设计和维护。&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;</description><pubDate>Wed, 26 Nov 2025 17:21:57 +0800</pubDate></item><item><title>HTTP请求解剖图：从请求头到请求体，小白秒懂</title><link>https://ajaa.cn/868.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172157176414891796880.jpg&quot; alt=&quot;HTTP请求解剖图：从请求头到请求体，小白秒懂&quot; title=&quot;HTTP请求解剖图：从请求头到请求体，小白秒懂&quot; /&gt;&lt;/p&gt;&lt;blockquote&gt; 
 &lt;p&gt;当你刷网页、点外卖、传文件时，浏览器都在悄悄发送&quot;网络快递&quot;——这就是&lt;strong&gt;HTTP请求&lt;/strong&gt;！它像精心包装的包裹，包含地址标签（请求头）和实际货物（请求体）。学会拆解这个包裹，你就掌握了网络通信的核心密码！&lt;/p&gt; 
&lt;/blockquote&gt; 
 
&lt;hr&gt; 
&lt;h4&gt;
📦 一、HTTP请求就像快递包裹&lt;/h4&gt; 
&lt;p&gt;想象你寄快递：&lt;/p&gt; 
 
&lt;ul&gt;
&lt;li&gt;📝 &lt;strong&gt;请求头(Headers)&lt;/strong&gt; = 快递单（写清收件地址、物品类型、特殊要求）&lt;/li&gt;
&lt;li&gt;🎁 &lt;strong&gt;请求体(Body)&lt;/strong&gt; = 包裹里的实际货物（表单数据、文件、JSON等）&lt;/li&gt;
&lt;/ul&gt; 
&lt;hr&gt; 
&lt;h4&gt;
📜 二、HTTP请求完整结构&lt;/h4&gt; 
&lt;p&gt;一个标准的HTTP请求包含三部分：&lt;/p&gt; 
 
&lt;h5&gt;
示例：登录请求的原始数据&lt;/h5&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-http&quot;&gt;POST /login HTTP/1.1                   👉 请求行
Host: www.example.***
Content-Type: application/json         👉 请求头
User-Agent: Chrome/115.0

{&quot;username&quot;:&quot;tom&quot;,&quot;password&quot;:&quot;123456&quot;} 👉 请求体
&lt;/code&gt;&lt;/pre&gt; 
&lt;hr&gt; 
&lt;h4&gt;
📋 三、请求头：包裹的&quot;快递单&quot;&lt;/h4&gt; 
&lt;h5&gt;
高频请求头大全（表格对比）&lt;/h5&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;请求头字段&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;常见值示例&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;目标服务器地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www.baidu.***&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User-Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;客户端身份标识&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Windows NT 10.0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content-Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;请求体的数据类型&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;application/json&lt;/code&gt;, &lt;code&gt;multipart/form-data&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A***ept&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;期望的响应格式&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;text/html&lt;/code&gt;, &lt;code&gt;image/*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authorization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;身份验证凭证&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bearer xxxxxx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cookie&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;客户端存储的数据&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sessionId=abc123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt; 
 &lt;p&gt;💡 关键提示：&lt;code&gt;Content-Type&lt;/code&gt;是请求体的&quot;说明书&quot;，服务器靠它解析数据格式！&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;hr&gt; 
&lt;h4&gt;
📦 四、请求体：包裹的&quot;实际货物&quot;&lt;/h4&gt; 
&lt;h5&gt;
根据Content-Type分三大类型：&lt;/h5&gt; 
&lt;h6&gt;
1. &lt;strong&gt;表单数据&lt;/strong&gt; (&lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;)&lt;/h6&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;网页表单提交的标准格式，如登录框&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-http&quot;&gt;POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=tom&amp;amp;password=123456  👉 键值对用&amp;amp;连接
&lt;/code&gt;&lt;/pre&gt; 
&lt;h6&gt;
2. &lt;strong&gt;JSON数据&lt;/strong&gt; (&lt;code&gt;application/json&lt;/code&gt;)&lt;/h6&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;API接口主流格式，结构清晰&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-http&quot;&gt;POST /api/users HTTP/1.1
Content-Type: application/json

{
  &quot;name&quot;: &quot;Lucy&quot;,
  &quot;age&quot;: 25,
  &quot;hobbies&quot;: [&quot;coding&quot;,&quot;reading&quot;]
}  👉 结构化数据
&lt;/code&gt;&lt;/pre&gt; 
&lt;h6&gt;
3. &lt;strong&gt;文件上传&lt;/strong&gt; (&lt;code&gt;multipart/form-data&lt;/code&gt;)&lt;/h6&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;上传图片/视频等二进制文件&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-http&quot;&gt;POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----boundary123

------boundary123
Content-Disposition: form-data; name=&quot;avatar&quot;; filename=&quot;photo.jpg&quot;
Content-Type: image/jpeg

&amp;lt;这里是图片的二进制数据...&amp;gt;
------boundary123-- 
&lt;/code&gt;&lt;/pre&gt; 
 
&lt;hr&gt; 
&lt;h4&gt;
🔍 五、如何查看HTTP请求？&lt;/h4&gt; 
&lt;h5&gt;
浏览器开发者工具实战&lt;/h5&gt; 
&lt;ol&gt;
&lt;li&gt;按 &lt;code&gt;F12&lt;/code&gt; 打开控制台 → &lt;code&gt;***work&lt;/code&gt; 标签&lt;/li&gt;
&lt;li&gt;刷新页面 → 点击任意请求&lt;/li&gt;
&lt;li&gt;查看 &lt;strong&gt;Headers&lt;/strong&gt; 和 &lt;strong&gt;Request Payload&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt; 
&lt;h5&gt;
代码示例：Python发送POST请求&lt;/h5&gt; 
&lt;pre&gt;&lt;code class=&quot;prism language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; requests

&lt;span class=&quot;token ***ment&quot;&gt;# 发送JSON请求体&lt;/span&gt;
headers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; requests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://api.example.***&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; json&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; headers&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token ***ment&quot;&gt;# 发送文件&lt;/span&gt;
files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#039;file&#039;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#039;report.xlsx&#039;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#039;rb&#039;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
requests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://upload.***&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; files&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;files&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; 
&lt;hr&gt; 
&lt;h4&gt;
🚀 六、不同请求方法的身体差异&lt;/h4&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;请求方法&lt;/th&gt;
&lt;th&gt;是否携带请求体&lt;/th&gt;
&lt;th&gt;典型场景&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GET&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 无&lt;/td&gt;
&lt;td&gt;获取网页、搜索查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;POST&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 有&lt;/td&gt;
&lt;td&gt;提交表单、创建资源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PUT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 有&lt;/td&gt;
&lt;td&gt;更新整个资源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PATCH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 有&lt;/td&gt;
&lt;td&gt;更新资源部分字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DELETE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ 通常无&lt;/td&gt;
&lt;td&gt;删除资源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt; 
 &lt;p&gt;⚠️ 注意：GET请求的参数在URL中传输：&lt;br&gt; &lt;code&gt;https://api.***/search?keyword=http&amp;amp;page=1&lt;/code&gt;&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;hr&gt; 
&lt;h4&gt;
💎 七、核心总结&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;请求行&lt;/strong&gt; = 动作指令（GET/POST等）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;请求头&lt;/strong&gt; = 元数据标签（Content-Type决定身体类型）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;请求体&lt;/strong&gt; = 传输的实际数据（JSON/表单/文件）&lt;/li&gt;
&lt;/ol&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;✨ 掌握HTTP请求结构，你就能：&lt;/p&gt; 
 &lt;ul&gt;
&lt;li&gt;调试API接口更高效 🛠️&lt;/li&gt;
&lt;li&gt;理解前后端数据交互 🔄&lt;/li&gt;
&lt;li&gt;快速定位网络问题 🔍&lt;/li&gt;
&lt;/ul&gt; 
&lt;/blockquote&gt; 
&lt;hr&gt; 
&lt;p&gt;&lt;strong&gt;📚 扩展阅读&lt;/strong&gt;&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;HTTP/2协议详解&lt;/li&gt;
&lt;li&gt;Postman接口测试工具实战&lt;/li&gt;
&lt;/ul&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;下次遇到API报错时，先检查请求头和请求体吧！&lt;/strong&gt; 👨‍💻&lt;br&gt; &lt;strong&gt;点赞 ▲ 收藏 ⭐ 关注 ➕ 素质三连走起~&lt;/strong&gt; ❤️&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;hr&gt;</description><pubDate>Wed, 26 Nov 2025 17:21:54 +0800</pubDate></item><item><title>异构多活架构引领医疗信创：浙人医案例解析</title><link>https://ajaa.cn/867.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172154176414891411795.jpg&quot; alt=&quot;异构多活架构引领医疗信创：浙人医案例解析&quot; title=&quot;异构多活架构引领医疗信创：浙人医案例解析&quot; /&gt;&lt;/p&gt;&lt;p&gt;作为浙江省卫健委直属，省内规模最大、实力最强的综合性三甲医院，浙江省人民医院（下称“浙人医”）庞大的服务体量与业务规模，使其成为省内卫健系统信创试点的核心选择，承担着探索和表率双重使命。电科金仓以“异构多活容灾架构”为核心的技术体系，不仅助力浙人医突破瓶颈，打造国内首个LIS系统国产化异构多院区多活改造案例，更构建了一套适配集团化医院信创的“全链路解决方案”，为行业提供了可落地的技术范本。&lt;/p&gt;

&lt;h3&gt;
一、集团化医院信创的三重难题&lt;/h3&gt;
&lt;p&gt;浙人医目前拥有朝晖、望江山、越城、富阳四大已运行院区、滨江、萧山两个在建院区及全面托管的八家分院，横跨杭州绍兴两地。浙人医的信创难点，源于其跨区域、多院区、高负荷的运营特性与医疗业务零中断、高安全的本质要求相互交织。此外由于各院区和分院在信息化建设初期技术能力、资源及政策要求等条件不一，受历史因素影响，各主体之间信息化建设的情况差异较大。信创启动前，浙人医院内并存Oracle、SQL Server、MySQL、PostgreSQL等多种数据库，院内的100余个业务系统由多个开发商建设，各系统对国产数据库的适配能力参差不齐。这些问题都需要借助信创契机一并解决。&lt;/p&gt;
&lt;p&gt;面对业务需求和政策要求带来的双重压力，浙人医最终选定电科金仓作为数据库合作伙伴，以LIS系统为突破口，开启信创之路。&lt;/p&gt;
&lt;p&gt;具体到数据库的选型来看，核心面临以下三重挑战：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;兼容性挑战&lt;/strong&gt;：集团化医院一体化管理模式要求各院区间信息化平台实现全面功能对接和数据共享，异构数据集成和互通的现实需求，对国产数据库的兼容能力提出高要求。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;业务连续性挑战&lt;/strong&gt;：浙人医LIS系统作为核心业务载体之一，支撑着全院日均2万余个标本的处理需求，任何停机都可能导致样本积压、报告延迟，甚至影响临床诊断决策，这对数据库的不停机迁移和数据同步能力提出严苛要求。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;高可用与信息安全挑战&lt;/strong&gt;：此前浙人医的LIS系统业务由越城院区主系统承载，系统压力大，院区间如遇网络中断需要手动拉起灾备系统。医疗数据的敏感性与合规要求下，浙人医提出“容灾恢复能力达到6级标准”（数据零丢失，RPO=0；故障恢复时间 RTO&amp;lt;10 分钟）和“业务连续性达到 99.99%”的硬性指标，同时需要进行密评改造。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
二、异构多活架构铸就安全底座&lt;/h3&gt;
&lt;p&gt;针对浙人医的痛点，电科金仓基于医疗行业特性与集团化医院需求，打造以“异构多活”为核心的技术路线，在完美替代原非信创业务系统的同时，让浙人医数据安全和处理效率有了质的提升。&lt;/p&gt;
&lt;p&gt;在浙人医LIS信创项目中，电科金仓实现了四大技术创新：&lt;/p&gt;
&lt;h4&gt;
1. 多活容灾保安全&lt;/h4&gt;
&lt;p&gt;电科金仓创新设计的异地多活容灾架构，核心在于“多院区互为主备”，彻底改变了浙人医此前面对数据安全的被动局面。目前浙人医拥有朝晖院区、越城院区和富阳院区三个数据中心，各中心之间互为主备，发生故障无需手动拉起灾备系统，可以实现秒级切换，帮助医院实现RTO≤10min、RPO=0的容灾目标，有效提高了业务系统的高可用性。同时各中心之间支持动态负载均衡，横向拆分了各院区压力，稳定性显著提升。&lt;/p&gt;

&lt;h4&gt;
2. 异构组网高兼容&lt;/h4&gt;
&lt;p&gt;当前浙人医数据库信创处在双轨并行状态，具体部署如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;朝晖院区：保持非信创系统活跃状态，金仓数据库仅承担局部业务正式访问；&lt;/li&gt;
&lt;li&gt;越城院区：以金仓数据库为主，非信创系统作为只读库；&lt;/li&gt;
&lt;li&gt;富阳院区（新建）：主数据库和只读库均为国产数据库，非信创系统仅用作数据备份。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;金仓数据库凭借其对非信创数据库的原生兼容能力，有效满足了异构组网需求。&lt;/p&gt;
&lt;h4&gt;
3. 多写同步提效率&lt;/h4&gt;
&lt;p&gt;浙人医三大数据中心支持双向多写。基于业务存量数据校验技术和增量数据校验技术，电科金仓异构数据同步软件KFS可提供全周期数据实时一致性校验且无需中断业务。三大数据中心通过KFS工具实现环状数据同步，异地保持了数据库的三个全量副本，进一步提升了容灾能力。&lt;/p&gt;
&lt;h4&gt;
4. 卫星方案降成本&lt;/h4&gt;
&lt;p&gt;此外浙人医还联合电科金仓，在小型院区重要工作位置布置轻量化卫星站，通过小型化节点保留核心功能，减少网络依赖，提升应急能力，在降低部署成本的同时进一步提升了系统可用性。&lt;/p&gt;
&lt;h3&gt;
三、项目成效与行业价值&lt;/h3&gt;
&lt;p&gt;LIS系统在多院区的成功落地，为浙人医信创建设的全面推进奠定了基础，其信创版图持续扩张。富阳院区作为全省首个医疗全栈信创样板间，更实现“云原生+国产化”双重突破。富阳院区试运行以来，运行高效稳定，核心指标表现优异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统每小时访问量达40多万次；&lt;/li&gt;
&lt;li&gt;数据库IOPS（每秒读写操作数）达到1万以上；&lt;/li&gt;
&lt;li&gt;与原系统相比，数据调用时间平均缩短了0.8秒，效率提升了60%；&lt;/li&gt;
&lt;li&gt;业务高峰时，系统响应延迟时间≤0.3秒，为患者和医护人员提供了更高效、更快速的服务体验。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
浙人医LIS系统关键操作性能对比表&lt;/h4&gt;

&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;NO.&lt;/th&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;设计性能指标&lt;/th&gt;
&lt;th&gt;非信创数据库实测&lt;/th&gt;
&lt;th&gt;金仓数据库实测&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;切换单元&lt;/td&gt;
&lt;td&gt;&amp;lt;2秒&lt;/td&gt;
&lt;td&gt;&amp;lt;2秒&lt;/td&gt;
&lt;td&gt;&amp;lt;2秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;样本列表刷新&lt;/td&gt;
&lt;td&gt;&amp;lt;1秒&lt;/td&gt;
&lt;td&gt;&amp;lt;1秒&lt;/td&gt;
&lt;td&gt;&amp;lt;1秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;切换样本&lt;/td&gt;
&lt;td&gt;20个项目以内&amp;lt;0.3秒；50个项目以内&amp;lt;0.5秒；有图形样本&amp;lt;0.5秒&lt;/td&gt;
&lt;td&gt;&amp;lt;0.15秒（后台接口返回：57ms）；&amp;lt;0.2秒（后台接口返回：63ms）；&amp;lt;0.2秒（后台接口返回：69ms）&lt;/td&gt;
&lt;td&gt;&amp;lt;0.15秒（后台接口返回：68ms）；&amp;lt;0.2秒（后台接口返回：79ms）；&amp;lt;0.2秒（后台接口返回：84ms）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;单样本审核&lt;/td&gt;
&lt;td&gt;&amp;lt;0.5秒&lt;/td&gt;
&lt;td&gt;&amp;lt;0.2秒（后台接口返回：130ms）&lt;/td&gt;
&lt;td&gt;&amp;lt;0.2秒（后台接口返回：127ms）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;批量样本审核&lt;/td&gt;
&lt;td&gt;&amp;lt;N*0.5秒&lt;/td&gt;
&lt;td&gt;&amp;lt;N*0.2秒&lt;/td&gt;
&lt;td&gt;&amp;lt;N*0.2秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;单样本发布&lt;/td&gt;
&lt;td&gt;&amp;lt;0.5秒&lt;/td&gt;
&lt;td&gt;&amp;lt;0.2秒（后台接口返回：90ms）&lt;/td&gt;
&lt;td&gt;&amp;lt;0.2秒（后台接口返回：124ms）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;批量样本发布&lt;/td&gt;
&lt;td&gt;&amp;lt;N*0.5秒&lt;/td&gt;
&lt;td&gt;&amp;lt;N*0.2秒&lt;/td&gt;
&lt;td&gt;&amp;lt;N*0.2秒&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;
四、案例启示与电科金仓实力&lt;/h3&gt;
&lt;p&gt;浙人医案例为集团化医院信创提供了清晰的可复制路径：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以LIS系统等业务重要、规模可控的系统为突破口，降低初期风险；&lt;/li&gt;
&lt;li&gt;采用电科金仓异构多活、双轨并行架构，平衡安全与效率；&lt;/li&gt;
&lt;li&gt;依托KFS等工具实现低侵扰改造，减少对现有业务的冲击。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;电科金仓凭借杰出的产品力和对医疗行业业务场景的深入理解，已经获得301医院、西京医院、常德二院等数十家医疗机构的认可，蝉联国产数据库销量榜首（点击了解详情）。此次与浙人医的合作，再次证明金仓数据库不仅能够替代国外产品，更能通过架构创新解决医疗场景的特殊需求。电科金仓的异构多活架构，也将持续为更多集团化医院破解信创难题，为构建自主可控的医疗信息化体系注入核心动力。&lt;/p&gt;
&lt;h3&gt;
五、电科金仓数据库常用sql语句&lt;/h3&gt;
&lt;h4&gt;
1. 新增数据（INSERT）&lt;/h4&gt;
&lt;p&gt;KingbaseES中使用INSERT语句向表中添加新数据，基本语法为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INTO&lt;/span&gt; 表名&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;字段&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 字段&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;VALUES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;值&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 值&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：向用户表（user_info）插入一条新记录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INTO&lt;/span&gt; user_info&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; username&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; age&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;VALUES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#039;张三&#039;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;25&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可同时插入多条记录，只需在VALUES后添加多组值，用逗号分隔。&lt;/p&gt;
&lt;h4&gt;
2. 查询数据（SELECT）&lt;/h4&gt;
&lt;p&gt;SELECT语句用于从表中查询数据，基本语法为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; 字段&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 字段&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; 表名 &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; 条件&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例1：查询用户表中所有记录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; user_info&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例2：查询年龄大于20的用户姓名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; username &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; user_info &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; age &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可通过ORDER BY进行排序，使用LIMIT限制返回条数。&lt;/p&gt;
&lt;h4&gt;
3. 更新数据（UPDATE）&lt;/h4&gt;
&lt;p&gt;UPDATE语句用于修改表中已有数据，基本语法为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt; 表名 &lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; 字段&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;新值&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 字段&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;新值&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; 条件&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：将id为1的用户年龄更新为26&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt; user_info &lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; age&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;26&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意必须添加WHERE条件，否则会更新表中所有记录。&lt;/p&gt;
&lt;h4&gt;
4. 删除数据（DELETE）&lt;/h4&gt;
&lt;p&gt;DELETE语句用于删除表中的记录，基本语法为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; 表名 &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; 条件&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：删除id为1的用户记录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;prism language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; user_info &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样需要注意WHERE条件，若省略将删除表中所有数据。&lt;/p&gt;
&lt;p&gt;以上四个操作构成了KingbaseES数据库的基本数据操作，通过合理组合使用这些语句，可以实现对数据库的完整管理。在实际应用中，需注意SQL语句的规范性和安全性，尤其是涉及删除和更新操作时要谨慎处理条件判断。&lt;/p&gt;</description><pubDate>Wed, 26 Nov 2025 17:21:52 +0800</pubDate></item><item><title>【Java 开发日记】RabbitMQ 里面的交换机是什么，你用过哪种？</title><link>https://ajaa.cn/866.html</link><description>&lt;p&gt;&lt;img src=&quot;https://img2.baidu.com/it/u=524507099,3476805100&amp;fm=253&amp;fmt=auto&amp;app=138&amp;f=JPEG?w=947&amp;h=500&quot; /&gt;&lt;/p&gt;&lt;p id=&quot;main-toc&quot; name=&quot;tableOfContents&quot;&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt; 
&lt;p id=&quot;%E4%BA%A4%E6%8D%A2%E6%9C%BA%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:80px&quot;&gt;交换机是什么？&lt;/p&gt; 
&lt;p id=&quot;%E6%88%91%E7%94%A8%E8%BF%87%E7%9A%84%E4%BA%A4%E6%8D%A2%E6%9C%BA%E7%B1%BB%E5%9E%8B%EF%BC%88%E5%8F%8A%E8%AF%A6%E7%BB%86%E4%BB%8B%E7%BB%8D%EF%BC%89-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:80px&quot;&gt;我用过的交换机类型（及详细介绍）&lt;/p&gt; 
&lt;p id=&quot;%E2%91%A0%20%E7%9B%B4%E8%BF%9E%E4%BA%A4%E6%8D%A2%E6%9C%BA-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;① 直连交换机&lt;/p&gt; 
&lt;p id=&quot;%E2%91%A1%20%E6%89%87%E5%87%BA%E4%BA%A4%E6%8D%A2%E6%9C%BA-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;② 扇出交换机&lt;/p&gt; 
&lt;p id=&quot;%E2%91%A2%20%E4%B8%BB%E9%A2%98%E4%BA%A4%E6%8D%A2%E6%9C%BA-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;③ 主题交换机&lt;/p&gt; 
&lt;p id=&quot;%E2%91%A3%20%E5%A4%B4%E4%BA%A4%E6%8D%A2%E6%9C%BA-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;④ 头交换机&lt;/p&gt; 
&lt;p id=&quot;%E6%80%BB%E7%BB%93%E4%B8%8E%E5%AF%B9%E6%AF%94-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:80px&quot;&gt;总结与对比&lt;/p&gt; 
&lt;hr&gt; 
&lt;h4 id=&quot;%E4%BA%A4%E6%8D%A2%E6%9C%BA%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F&quot; name=&quot;%E4%BA%A4%E6%8D%A2%E6%9C%BA%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F&quot;&gt;&lt;span style=&quot;color:#4da8ee&quot;&gt;交换机是什么？&lt;/span&gt;&lt;/h4&gt; 
&lt;p&gt;在 RabbitMQ 中，&lt;strong&gt;交换机&lt;/strong&gt; 是消息路由机制的核心。你可以把它想象成一个&lt;strong&gt;邮局分拣员&lt;/strong&gt;。&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;生产者&lt;/strong&gt; 发送消息时，它不是直接把消息放到队列里，而是发送到 &lt;strong&gt;交换机&lt;/strong&gt;。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;然后，交换机根据一个特定的规则（这个规则叫做 &lt;strong&gt;“绑定”&lt;/strong&gt; 和 &lt;strong&gt;“路由键”&lt;/strong&gt;）来决定把消息投递到哪些队列中。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;绑定&lt;/strong&gt; 是连接交换机和队列的桥梁，你可以为这个桥梁设定一个路由键。&lt;/p&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;&lt;strong&gt;简单流程：&lt;/strong&gt;&lt;br&gt; &lt;code&gt;生产者 -&amp;gt; 交换机 -&amp;gt; (根据绑定规则) -&amp;gt; 一个或多个队列 -&amp;gt; 消费者&lt;/code&gt;&lt;/p&gt; 
&lt;p&gt;如果没有交换机，生产者需要直接知道所有队列的存在，这在复杂的、需要灵活路由的系统里是几乎不可行的。交换机解耦了生产者和队列，使得消息的路由策略变得非常灵活和强大。&lt;/p&gt; 
&lt;h4 id=&quot;%E6%88%91%E7%94%A8%E8%BF%87%E7%9A%84%E4%BA%A4%E6%8D%A2%E6%9C%BA%E7%B1%BB%E5%9E%8B%EF%BC%88%E5%8F%8A%E8%AF%A6%E7%BB%86%E4%BB%8B%E7%BB%8D%EF%BC%89&quot; name=&quot;%E6%88%91%E7%94%A8%E8%BF%87%E7%9A%84%E4%BA%A4%E6%8D%A2%E6%9C%BA%E7%B1%BB%E5%9E%8B%EF%BC%88%E5%8F%8A%E8%AF%A6%E7%BB%86%E4%BB%8B%E7%BB%8D%EF%BC%89&quot;&gt;&lt;span style=&quot;color:#4da8ee&quot;&gt;我用过的交换机类型（及详细介绍）&lt;/span&gt;&lt;/h4&gt; 
&lt;p&gt;RabbitMQ 主要提供了四种类型的交换机，每种都有不同的路由行为。&lt;strong&gt;最常用的是前三种&lt;/strong&gt;。&lt;/p&gt; 
&lt;h5 id=&quot;%E2%91%A0%20%E7%9B%B4%E8%BF%9E%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot; name=&quot;%E2%91%A0%20%E7%9B%B4%E8%BF%9E%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot;&gt;&lt;strong&gt;① 直连交换机&lt;/strong&gt;&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt;：&lt;code&gt;direct&lt;/code&gt;&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;行为&lt;/strong&gt;：一个消息的路由键如果 &lt;strong&gt;完全匹配&lt;/strong&gt; 某个队列的绑定键，那么这条消息就会被路由到该队列。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;类比&lt;/strong&gt;：就像公司里的邮件系统，你写对了工号（路由键），邮件就会被准确地投递到那个人的邮箱（队列）。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：&lt;/p&gt; 
  &lt;ul&gt;&lt;li&gt; &lt;p&gt;&lt;strong&gt;有明确一对一或一对多任务分发的场景&lt;/strong&gt;。例如，将错误日志（&lt;code&gt;routing_key: &#039;error&#039;&lt;/code&gt;）只发送给记录错误的队列，将订单消息（&lt;code&gt;routing_key: &#039;order.paid&#039;&lt;/code&gt;）只发送给处理已支付订单的队列。&lt;/p&gt; &lt;/li&gt;&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;我的使用经验&lt;/strong&gt;：这是最常用、最简单的交换机类型。我们在处理不同优先级的任务时经常使用，比如 &lt;code&gt;task.high&lt;/code&gt; 和 &lt;code&gt;task.low&lt;/code&gt; 分别绑定到不同的队列，由不同性能的消费者来处理。&lt;/p&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;h5 id=&quot;%E2%91%A1%20%E6%89%87%E5%87%BA%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot; name=&quot;%E2%91%A1%20%E6%89%87%E5%87%BA%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot;&gt;&lt;strong&gt;② 扇出交换机&lt;/strong&gt;&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt;：&lt;code&gt;fanout&lt;/code&gt;&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;行为&lt;/strong&gt;：它会把发送到该交换机的所有消息 &lt;strong&gt;广播&lt;/strong&gt; 到所有与它绑定的队列中。&lt;strong&gt;它完全忽略路由键&lt;/strong&gt;。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;类比&lt;/strong&gt;：就像公司里的群发邮件或公告板，一条消息发出，所有订阅了的人都收到一份。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：&lt;/p&gt; 
  &lt;ul&gt;&lt;li&gt; &lt;p&gt;&lt;strong&gt;发布/订阅模式&lt;/strong&gt;。例如，用户成功注册后，需要同时执行多个操作：发送欢迎邮件、初始化用户画像、发放新手优惠券。你可以让这三个任务对应的队列都绑定到同一个 &lt;code&gt;fanout&lt;/code&gt; 交换机上，这样一条“用户注册成功”的消息会被这三个队列同时接收并处理。&lt;/p&gt; &lt;/li&gt;&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;我的使用经验&lt;/strong&gt;：在需要做事件驱动架构，一个事件触发多个下游服务的场景下非常好用。我们用它来同步不同服务间的缓存数据失效通知。&lt;/p&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;h5 id=&quot;%E2%91%A2%20%E4%B8%BB%E9%A2%98%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot; name=&quot;%E2%91%A2%20%E4%B8%BB%E9%A2%98%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot;&gt;&lt;strong&gt;③ 主题交换机&lt;/strong&gt;&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt;：&lt;code&gt;topic&lt;/code&gt;&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;行为&lt;/strong&gt;：功能最强大的交换机。它使用路由键和一种模式进行匹配。绑定键（Binding Key）可以包含两种特殊的通配符：&lt;/p&gt; 
  &lt;ul&gt;
&lt;li&gt; &lt;p&gt;&lt;code&gt;*&lt;/code&gt; (星号)：匹配一个单词。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;code&gt;#&lt;/code&gt; (井号)：匹配零个或多个单词。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;单词之间用点号 &lt;code&gt;.&lt;/code&gt; 分隔，例如 &lt;code&gt;usa.news&lt;/code&gt;， &lt;code&gt;europe.weather.serious&lt;/code&gt;。&lt;/p&gt; &lt;/li&gt;
&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;类比&lt;/strong&gt;：就像新闻订阅系统，你可以订阅“所有美国地区的新闻”（&lt;code&gt;usa.#&lt;/code&gt;），或者“所有地区的严重天气”（&lt;code&gt;*.weather.serious&lt;/code&gt;）。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：&lt;/p&gt; 
  &lt;ul&gt;&lt;li&gt; &lt;p&gt;&lt;strong&gt;根据消息的多个属性进行灵活路由&lt;/strong&gt;。例如，一个日志处理系统，你可以根据日志的严重程度（&lt;code&gt;info&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;）和来源（&lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;order&lt;/code&gt;, &lt;code&gt;payment&lt;/code&gt;）来路由。绑定键 &lt;code&gt;*.error&lt;/code&gt; 会接收所有服务的错误日志，而 &lt;code&gt;order.*&lt;/code&gt; 会接收订单服务的所有日志。&lt;/p&gt; &lt;/li&gt;&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;我的使用经验&lt;/strong&gt;：在构建复杂的消息路由规则时，&lt;code&gt;topic&lt;/code&gt; 交换机是首选。我们用它来构建日志收集和业务通知系统，可以根据不同的标签组合将消息精准地投递给感兴趣的消费者。&lt;/p&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;h5 id=&quot;%E2%91%A3%20%E5%A4%B4%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot; name=&quot;%E2%91%A3%20%E5%A4%B4%E4%BA%A4%E6%8D%A2%E6%9C%BA&quot;&gt;&lt;strong&gt;④ 头交换机&lt;/strong&gt;&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt;：&lt;code&gt;headers&lt;/code&gt;&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;行为&lt;/strong&gt;：它不依赖于路由键的匹配规则，而是根据消息的 &lt;strong&gt;headers 属性&lt;/strong&gt; 来路由。在绑定时，你需要指定一组键值对。当发送来的消息的 headers 属性与绑定时指定的键值对完全匹配时，消息就会被路由到该队列。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：&lt;/p&gt; 
  &lt;ul&gt;&lt;li&gt; &lt;p&gt;用于需要基于多个消息属性（而不仅仅是一个路由键）进行路由的复杂场景。但因为性能比 &lt;code&gt;topic&lt;/code&gt; 交换机差，且使用起来更复杂，所以&lt;strong&gt;在实际开发中非常少见&lt;/strong&gt;。&lt;/p&gt; &lt;/li&gt;&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;&lt;strong&gt;我的使用经验&lt;/strong&gt;：我个人在实际项目中几乎没有使用过头交换机，&lt;code&gt;topic&lt;/code&gt; 交换机已经能满足绝大多数复杂路由的需求，而且更直观高效。&lt;/p&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;h4 id=&quot;%E6%80%BB%E7%BB%93%E4%B8%8E%E5%AF%B9%E6%AF%94&quot; name=&quot;%E6%80%BB%E7%BB%93%E4%B8%8E%E5%AF%B9%E6%AF%94&quot;&gt;&lt;span style=&quot;color:#4da8ee&quot;&gt;总结与对比&lt;/span&gt;&lt;/h4&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;交换机类型&lt;/th&gt;
&lt;th&gt;路由行为&lt;/th&gt;
&lt;th&gt;使用场景&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;直连 (direct)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;精确匹配 &lt;code&gt;routing_key&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;任务分发、RPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;扇出 (fanout)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;广播给所有绑定队列&lt;/td&gt;
&lt;td&gt;发布/订阅、事件广播&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;主题 (topic)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通配符匹配 &lt;code&gt;routing_key&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;灵活的多维消息路由&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;头 (headers)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;匹配 &lt;code&gt;headers&lt;/code&gt; 属性&lt;/td&gt;
&lt;td&gt;复杂属性匹配（不常用）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt; 
&lt;p&gt;&lt;strong&gt;回答“你用过哪种？”：&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;在实际工作中，我最常用的是 &lt;strong&gt;直连交换机&lt;/strong&gt;、&lt;strong&gt;扇出交换机&lt;/strong&gt; 和 &lt;strong&gt;主题交换机&lt;/strong&gt;。&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt; &lt;p&gt;用 &lt;strong&gt;&lt;code&gt;direct&lt;/code&gt;&lt;/strong&gt; 处理简单的任务分发。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;用 &lt;strong&gt;&lt;code&gt;fanout&lt;/code&gt;&lt;/strong&gt; 处理需要广播消息的发布/订阅场景。&lt;/p&gt; &lt;/li&gt;
&lt;li&gt; &lt;p&gt;用 &lt;strong&gt;&lt;code&gt;topic&lt;/code&gt;&lt;/strong&gt; 处理需要根据多种条件（如日志级别和模块）进行灵活路由的复杂业务。&lt;/p&gt; &lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;p id=&quot;u26295433&quot;&gt;如果小假的内容对你有帮助，请&lt;span style=&quot;color:#fe2c24&quot;&gt;&lt;strong&gt;点赞&lt;/strong&gt;&lt;/span&gt;，&lt;span style=&quot;color:#fe2c24&quot;&gt;&lt;strong&gt;评论&lt;/strong&gt;&lt;/span&gt;，&lt;span style=&quot;color:#fe2c24&quot;&gt;&lt;strong&gt;收藏&lt;/strong&gt;&lt;/span&gt;。创作不易，大家的支持就是我坚持下去的动力！&lt;/p&gt; 
</description><pubDate>Wed, 26 Nov 2025 17:21:50 +0800</pubDate></item><item><title>对比nginx、kong、apisix、zuul、gateway网关</title><link>https://ajaa.cn/865.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172150176414891046267.jpg&quot; alt=&quot;对比nginx、kong、apisix、zuul、gateway网关&quot; title=&quot;对比nginx、kong、apisix、zuul、gateway网关&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

&lt;br&gt; Nginx、Kong、APISIX、Zuul、Spring Cloud Gateway 均是 API 网关或反向代理工具，但定位、技术栈和适用场景差异显著。以下从核心定位、优缺点、适用场景三个维度对比分析：
&lt;p&gt;&lt;/p&gt; 
&lt;h4&gt;
1. Nginx&lt;/h4&gt; 
&lt;h5&gt;
核心定位&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;高性能 HTTP 服务器、反向代理服务器、负载均衡器，用 C 语言开发，是网关领域的“基础设施”。&lt;/li&gt;
&lt;li&gt;主要功能：静态资源托管、反向代理、负载均衡、SSL 终结、简单路由转发。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
优点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;性能极强&lt;/strong&gt;：C 语言编写，异步非阻塞架构，单机支持数十万并发，资源消耗低。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;稳定性高&lt;/strong&gt;：经过数十年验证，适合作为流量入口的“第一道防线”。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;功能丰富&lt;/strong&gt;：支持 URL 重写、缓存、限流（简单）、SSL 配置等基础网关能力。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;生态成熟&lt;/strong&gt;：大量第三方模块（如 &lt;code&gt;ngx_lua&lt;/code&gt;）可扩展功能，社区文档丰富。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
缺点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;动态配置弱&lt;/strong&gt;：修改配置需重启或 reload（热加载但有短暂阻塞风险），不适合频繁变更的场景。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API 网关特性弱&lt;/strong&gt;：缺乏原生的服务发现、细粒度限流、熔断、监控等微服务网关功能，需通过 Lua 脚本或第三方模块扩展，开发成本高。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;开发门槛高&lt;/strong&gt;：扩展功能需熟悉 Lua 或 C 模块，对运维人员要求高。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
适用场景&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;作为全局流量入口（边缘网关），处理静态资源、SSL 终结、初步负载均衡。&lt;/li&gt;
&lt;li&gt;搭配 &lt;code&gt;ngx_lua&lt;/code&gt; 实现简单的 API 路由和限流（如 OpenResty 方案）。&lt;/li&gt;
&lt;li&gt;对性能要求极高、配置变更不频繁的场景（如电商大促流量入口）。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h4&gt;
2. Kong&lt;/h4&gt; 
&lt;h5&gt;
核心定位&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;基于 Nginx + OpenResty（Lua 扩展）的开源 API 网关，专注于 API 全生命周期管理。&lt;/li&gt;
&lt;li&gt;主要功能：API 路由、负载均衡、认证授权、限流熔断、监控日志、插件扩展。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
优点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;性能优秀&lt;/strong&gt;：基于 Nginx 内核，性能接近原生 Nginx，支持高并发。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;插件生态丰富&lt;/strong&gt;：内置 100+ 插件（如 JWT 认证、OAuth2、限流、监控），支持自定义 Lua 插件。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;动态配置&lt;/strong&gt;：通过 Admin API 实时修改配置（无需重启），适合动态扩缩容的场景。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;支持服务发现&lt;/strong&gt;：可对接 Consul、etcd、Kuber***es 等注册中心。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
缺点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;学习成本高&lt;/strong&gt;：插件开发依赖 Lua，运维和扩展需熟悉 OpenResty 生态。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;资源消耗较高&lt;/strong&gt;：相比原生 Nginx 略重，内存占用更大。&lt;/li&gt;
&lt;li&gt;企业级功能（如多租户、高级监控）需依赖商业版（Kong Enterprise）。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
适用场景&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;中小型微服务架构的 API 网关，需要丰富的插件功能（如认证、限流）。&lt;/li&gt;
&lt;li&gt;混合架构（既有传统服务也有微服务）的统一 API 入口。&lt;/li&gt;
&lt;li&gt;对动态配置和可扩展性有要求，但团队能接受 Lua 技术栈的场景。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h4&gt;
3. APISIX&lt;/h4&gt; 
&lt;h5&gt;
核心定位&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;云原生、高性能的开源 API 网关，基于 Nginx + etcd（配置存储），由中国团队开发，兼容 OpenResty 生态。&lt;/li&gt;
&lt;li&gt;主要功能：动态路由、负载均衡、限流熔断、服务发现、监控告警、多语言插件。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
优点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;性能顶尖&lt;/strong&gt;：基于 Nginx 内核，采用 etcd 存储配置，动态配置生效速度快（毫秒级），性能略优于 Kong。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;云原生友好&lt;/strong&gt;：原生支持 Kuber***es、服务网格（Service Mesh），适合容器化部署。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;插件生态灵活&lt;/strong&gt;：支持 Lua、Go、Java 等多语言开发插件，内置丰富的微服务相关插件（如 SkyWalking 追踪、普罗米修斯监控）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;中文支持好&lt;/strong&gt;：文档和社区有大量中文资源，国内用户适配成本低。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
缺点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;生态成熟度略逊于 Kong（插件数量和社区规模较小）。&lt;/li&gt;
&lt;li&gt;部分高级功能（如多集群管理）仍在迭代中。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
适用场景&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;云原生/微服务架构，尤其是 Kuber***es 环境下的 API 网关。&lt;/li&gt;
&lt;li&gt;对动态配置速度和性能要求极高的场景（如金融、电商核心服务）。&lt;/li&gt;
&lt;li&gt;国内团队（中文文档和社区支持更友好）。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h4&gt;
4. Zuul&lt;/h4&gt; 
&lt;h5&gt;
核心定位&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;基于 Java 的开源 API 网关，由 ***flix 开发，是 Spring Cloud 早期的默认网关组件（Zuul 1.x），后推出 Zuul 2.x（异步非阻塞）。&lt;/li&gt;
&lt;li&gt;主要功能：路由转发、认证授权、负载均衡、简单限流。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
优点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spring Cloud 生态集成&lt;/strong&gt;：与 Spring Boot 无缝衔接，适合 Java 技术栈团队快速上手。&lt;/li&gt;
&lt;li&gt;开发门槛低：用 Java 开发过滤器，符合 Java 开发者习惯。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
缺点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;性能较差&lt;/strong&gt;：Zuul 1.x 是同步阻塞架构，高并发下性能瓶颈明显（已被 Spring Cloud Gateway 替代）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;功能简陋&lt;/strong&gt;：缺乏高级特性（如细粒度限流、动态配置），需大量自定义开发。&lt;/li&gt;
&lt;li&gt;社区活跃度低：Zuul 2.x 发布缓慢，生态逐渐萎缩，已不是主流选择。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
适用场景&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;早期 Spring Cloud 项目的过渡期网关（新项目不推荐）。&lt;/li&gt;
&lt;li&gt;低并发、对性能要求不高的小型 Java 微服务架构。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h4&gt;
5. Spring Cloud Gateway&lt;/h4&gt; 
&lt;h5&gt;
核心定位&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;基于 Spring Boot 2.x、Spring WebFlux（响应式编程）的开源 API 网关，是 Spring Cloud 官方推荐的网关（替代 Zuul）。&lt;/li&gt;
&lt;li&gt;主要功能：动态路由、负载均衡、熔断限流（集成 Resilience4j/Sentinel）、服务发现（集成 Eureka/Consul）、监控追踪。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
优点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spring 生态无缝集成&lt;/strong&gt;：天然支持 Spring Cloud 组件（如服务发现、配置中心），Java 团队零学习成本。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;性能优秀&lt;/strong&gt;：基于 ***ty 异步非阻塞架构，性能远超 Zuul 1.x，接近 Nginx 级（但略低于 Kong/APISIX）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;功能丰富&lt;/strong&gt;：内置路由断言、过滤器（如 JWT 认证、请求重写），支持动态配置（结合 Spring Cloud Config）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;开发便捷&lt;/strong&gt;：用 Java/Kotlin 开发自定义过滤器，符合微服务开发习惯。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
缺点&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;仅支持 Java 技术栈，对非 Spring 项目适配性差。&lt;/li&gt;
&lt;li&gt;性能略逊于 Nginx/Kong/APISIX（JVM 语言的天然开销）。&lt;/li&gt;
&lt;li&gt;依赖 Spring 生态，灵活性受限于框架设计。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h5&gt;
适用场景&lt;/h5&gt; 
&lt;ul&gt;
&lt;li&gt;纯 Spring Cloud 微服务架构，需要与 Java 技术栈深度融合的场景。&lt;/li&gt;
&lt;li&gt;对开发效率要求高，需快速集成服务发现、熔断等微服务特性的场景。&lt;/li&gt;
&lt;li&gt;中高并发场景（性能满足大多数企业级需求）。&lt;/li&gt;
&lt;/ul&gt; 
&lt;h4&gt;
对比总结表&lt;/h4&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;th&gt;Kong&lt;/th&gt;
&lt;th&gt;APISIX&lt;/th&gt;
&lt;th&gt;Zuul&lt;/th&gt;
&lt;th&gt;Spring Cloud Gateway&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;技术栈&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C/Lua&lt;/td&gt;
&lt;td&gt;Nginx + Lua&lt;/td&gt;
&lt;td&gt;Nginx + Lua + etcd&lt;/td&gt;
&lt;td&gt;Java（同步阻塞）&lt;/td&gt;
&lt;td&gt;Java（***ty 响应式）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;性能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;极高（10万+ QPS）&lt;/td&gt;
&lt;td&gt;高（接近 Nginx）&lt;/td&gt;
&lt;td&gt;高（略优于 Kong）&lt;/td&gt;
&lt;td&gt;低（万级 QPS）&lt;/td&gt;
&lt;td&gt;中高（5万+ QPS）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;动态配置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;弱（需 reload）&lt;/td&gt;
&lt;td&gt;强（Admin API）&lt;/td&gt;
&lt;td&gt;极强（etcd 实时同步）&lt;/td&gt;
&lt;td&gt;弱（需重启）&lt;/td&gt;
&lt;td&gt;强（配置中心集成）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;生态&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最成熟（第三方模块）&lt;/td&gt;
&lt;td&gt;丰富（100+ 插件）&lt;/td&gt;
&lt;td&gt;快速成长（多语言插件）&lt;/td&gt;
&lt;td&gt;萎缩（基本停滞）&lt;/td&gt;
&lt;td&gt;完善（Spring 生态）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;开发成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高（Lua/C）&lt;/td&gt;
&lt;td&gt;中（Lua）&lt;/td&gt;
&lt;td&gt;中（多语言支持）&lt;/td&gt;
&lt;td&gt;低（Java）&lt;/td&gt;
&lt;td&gt;低（Java/Spring）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;边缘网关、静态资源&lt;/td&gt;
&lt;td&gt;混合架构 API 网关&lt;/td&gt;
&lt;td&gt;云原生/高性能网关&lt;/td&gt;
&lt;td&gt;老旧 Spring 项目&lt;/td&gt;
&lt;td&gt;Spring 微服务网关&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;
选型建议&lt;/h4&gt; 
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;追求极致性能 + 静态资源/边缘网关&lt;/strong&gt;：选 &lt;strong&gt;Nginx&lt;/strong&gt;（搭配 OpenResty 扩展）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;云原生/高动态配置 + 多语言支持&lt;/strong&gt;：选 &lt;strong&gt;APISIX&lt;/strong&gt;（国内团队优先）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;插件生态丰富 + 混合架构&lt;/strong&gt;：选 &lt;strong&gt;Kong&lt;/strong&gt;（商业版适合企业级需求）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;纯 Spring Cloud 微服务&lt;/strong&gt;：选 &lt;strong&gt;Spring Cloud Gateway&lt;/strong&gt;（开发效率最高）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;避免使用&lt;/strong&gt;：Zuul（性能差，已被替代）。&lt;/li&gt;
&lt;/ul&gt;</description><pubDate>Wed, 26 Nov 2025 17:21:47 +0800</pubDate></item><item><title>Spring AI</title><link>https://ajaa.cn/864.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172147176414890712540.jpg&quot; alt=&quot;Spring AI&quot; title=&quot;Spring AI&quot; /&gt;&lt;/p&gt;&lt;p id=&quot;main-toc&quot; name=&quot;tableOfContents&quot;&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt; 
&lt;p id=&quot;%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:0px&quot;&gt;基本概念&lt;/p&gt; 
&lt;p id=&quot;%E4%BB%80%E4%B9%88%E6%98%AF%20AI-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;什么是 AI&lt;/p&gt; 
&lt;p id=&quot;%E6%A8%A1%E5%9E%8B%EF%BC%88Model%EF%BC%89-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;模型（Model）&lt;/p&gt; 
&lt;p id=&quot;%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%20-%20Large%20Language%20Model%20(LLM)-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;大语言模型  (LLM)&lt;/p&gt; 
&lt;p id=&quot;%E6%8F%90%E7%A4%BA%E8%AF%8D%20%EF%BC%88Prompt%EF%BC%89-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;提示词 （Prompt）&lt;/p&gt; 
&lt;p id=&quot;%E8%AF%8D%E5%85%83%EF%BC%88Token%EF%BC%89-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;词元（Token）&lt;/p&gt; 
&lt;p id=&quot;Spring%20AI%20%E6%98%AF%E4%BB%80%E4%B9%88-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;Spring AI 是什么&lt;/p&gt; 
&lt;p id=&quot;%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:0px&quot;&gt;快速入门&lt;/p&gt; 
&lt;p id=&quot;%E7%8E%AF%E5%A2%83%E8%A6%81%E6%B1%82-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;环境要求&lt;/p&gt; 
&lt;p id=&quot;%E7%94%B3%E8%AF%B7%20API%20Key-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;申请 API Key&lt;/p&gt; 
&lt;p id=&quot;%E9%A1%B9%E7%9B%AE%E5%88%9B%E5%BB%BA-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;项目创建&lt;/p&gt; 
&lt;p id=&quot;%E6%8E%A5%E5%8F%A3%E7%BC%96%E5%86%99-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;接口编写&lt;/p&gt; 
&lt;p id=&quot;%E6%A0%B8%E5%BF%83%E6%8E%A5%E5%8F%A3-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:0px&quot;&gt;核心接口&lt;/p&gt; 
&lt;p id=&quot;ChatModel%C2%A0-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;ChatModel &lt;/p&gt; 
&lt;p id=&quot;ChatClient-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;ChatClient&lt;/p&gt; 
&lt;p id=&quot;%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:0px&quot;&gt;消息类型&lt;/p&gt; 
&lt;p id=&quot;SystemMessage-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;SystemMessage&lt;/p&gt; 
&lt;p id=&quot;UserMessage-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;UserMessage&lt;/p&gt; 
&lt;p id=&quot;AssistantMessage-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;AssistantMessage&lt;/p&gt; 
&lt;p id=&quot;%E8%BE%93%E5%87%BA%E6%A0%BC%E5%BC%8F-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:0px&quot;&gt;输出格式&lt;/p&gt; 
&lt;p id=&quot;%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BA-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;结构化输出&lt;/p&gt; 
&lt;p id=&quot;%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:40px&quot;&gt;流式输出&lt;/p&gt; 
&lt;p id=&quot;SSE%20%E5%8D%8F%E8%AE%AE%E4%BB%8B%E7%BB%8D-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:80px&quot;&gt;SSE 协议介绍&lt;/p&gt; 
&lt;p id=&quot;SSE%20%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:80px&quot;&gt;SSE 数据格式&lt;/p&gt; 
&lt;p id=&quot;data-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;data&lt;/p&gt; 
&lt;p id=&quot;event-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;event&lt;/p&gt; 
&lt;p id=&quot;id-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;id&lt;/p&gt; 
&lt;p id=&quot;retry-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:120px&quot;&gt;retry&lt;/p&gt; 
&lt;p id=&quot;SSE%20%E5%8D%8F%E8%AE%AE%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:80px&quot;&gt;SSE 使用示例&lt;/p&gt; 
&lt;p id=&quot;Flux-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:80px&quot;&gt;Flux&lt;/p&gt; 
&lt;p id=&quot;Advisors-toc&quot; name=&quot;tableOfContents&quot; style=&quot;margin-left:0px&quot;&gt;Advisors&lt;/p&gt; 
&lt;hr id=&quot;hr-toc&quot; name=&quot;tableOfContents&quot;&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5&quot; name=&quot;%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5&quot; style=&quot;background-color:transparent&quot;&gt;基本概念&lt;/h2&gt; 
&lt;h3 id=&quot;%E4%BB%80%E4%B9%88%E6%98%AF%20AI&quot; name=&quot;%E4%BB%80%E4%B9%88%E6%98%AF%20AI&quot;&gt;什么是 AI&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;AI&lt;/strong&gt;：也就是 &lt;strong&gt;人工智能（Artificial Intelligence）&lt;/strong&gt;，顾名思义，就是&lt;strong&gt;让机器模拟人类智能的科学与技术&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;我们通过一个示例来对比理解：&lt;/strong&gt;&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;普通计算机程序：&lt;/strong&gt;像一台自动售货机。你按下特定的按钮（输入），它就给你一瓶特定的饮料（输出）。它的&lt;strong&gt;所有行为都是程序员预先设定好的规则&lt;/strong&gt;&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;人工智能程序&lt;/strong&gt;：更像是一个正在学习的孩子。你给它看很多猫和狗的图片（数据），并告诉它哪个是猫，哪个是狗。经过学习后，当你给它一张它从未从未见过的猫咪图片时，它也能识别出来。它&lt;strong&gt;自己从数据中学会了规律&lt;/strong&gt;，而不是依赖硬编码的规则&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;因此，AI 的核心是&lt;strong&gt;从经验中学习，并根据所学做出决策或预测&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;而目前最主流、最引人注目的 AI 分支是 &lt;strong&gt;生成式人工智能&lt;/strong&gt;，也就是现在常说的&lt;strong&gt; AIGC（Artificial Intelligence Generated Content，人工智能生成内容）&lt;/strong&gt;。它与传统 AI（主要用于分析数据，比如识别人脸）不同，它的目标是&lt;strong&gt;利用人工智能技术自动生成或创造出各类数字内容&lt;/strong&gt;，比如写文章、报告、翻译、编程&lt;/p&gt; 
&lt;p&gt;为了更好的理解 AI，我们先来理解其中的一些&lt;strong&gt;常见术语&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E6%A8%A1%E5%9E%8B%EF%BC%88Model%EF%BC%89&quot; name=&quot;%E6%A8%A1%E5%9E%8B%EF%BC%88Model%EF%BC%89&quot;&gt;模型（Model）&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;模型（Model）&lt;/strong&gt;是 &lt;strong&gt;AI系统的核心&lt;/strong&gt;，它是&lt;strong&gt;通过算法在数据数据上训练后得到的结果&lt;/strong&gt;。模型本质上是一个&lt;strong&gt;数学函数&lt;/strong&gt;，它接收输入数据，并进行计算，然后产生输出。&lt;/p&gt; 
&lt;p&gt;我们常说的“调用一个AI”，实际上就是在使用这个“模型”。模型文件大小不一，可以从几MB到几十GB&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;可以把 AI 模型想象成一个 &lt;strong&gt;“虚拟大脑”&lt;/strong&gt;。这个大脑通过在大量数据上进行“训练”或“学习”，掌握了一些技能和知识。而当被提问时，就需要运用这个大脑掌握的知识来解决问题&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%20-%20Large%20Language%20Model%20(LLM)&quot; name=&quot;%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%20-%20Large%20Language%20Model%20(LLM)&quot; style=&quot;background-color:transparent&quot;&gt;大语言模型  (LLM)&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;LLM（&lt;/strong&gt; &lt;strong&gt;Large Language Model，大语言模型）&lt;/strong&gt;：一种基于&lt;strong&gt;深度学习&lt;/strong&gt;的、使用海量文本数据训练的&lt;strong&gt;模型&lt;/strong&gt;。它的主要任务是理解和生成人类语言。LLM是当前生成式AI热潮的代表。它们的特点是“&lt;strong&gt;大&lt;/strong&gt;”，体现在&lt;strong&gt;训练数据量大、模型参数数量巨大&lt;/strong&gt;&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;可以将其看做一个进行了超大规模训练的 &lt;strong&gt;“专家大脑”&lt;/strong&gt; 。它通过学习互联网上几乎所有的文本，掌握了语言的语法、句法、事实知识以及上下文逻辑，拥有数百亿甚至数千亿个参数，并且因为它什么都学过，所以能应对各种各样的话题和任务&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E6%8F%90%E7%A4%BA%E8%AF%8D%20%EF%BC%88Prompt%EF%BC%89&quot; name=&quot;%E6%8F%90%E7%A4%BA%E8%AF%8D%20%EF%BC%88Prompt%EF%BC%89&quot;&gt;提示词 （Prompt）&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;提示词（Prompt）：&lt;/strong&gt;用户提供给AI模型的&lt;strong&gt;指令、问题或上下文信息&lt;/strong&gt;。模型根据提示词来生成相应的回复。提示词的质量直接决定了 AI 回答的质量。&lt;/p&gt; 
&lt;p&gt;而设计和优化提示词的过程被称为“&lt;strong&gt;提示词工程&lt;/strong&gt;”，是一门新兴的技能。&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;提示词就像是给AI这位“天才天才”下达的“&lt;strong&gt;工作订单”&lt;/strong&gt;。订单越清晰、越具体，完成的工作质量就越高。&lt;/p&gt; 
 &lt;p&gt;例如：&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;简单提示词&lt;/strong&gt;：“法国的首都是哪里？” -&amp;gt; 模型回答：“巴黎。”&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;复杂提示词（角色扮演）&lt;/strong&gt;：“假设你是一位资深营养师，请为我（一位办公室久坐的上班族）设计一份为期一周的健康午餐食谱。” -&amp;gt; 模型会以营养师的口吻提供一份详细的食谱。&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E8%AF%8D%E5%85%83%EF%BC%88Token%EF%BC%89&quot; name=&quot;%E8%AF%8D%E5%85%83%EF%BC%88Token%EF%BC%89&quot;&gt;词元（Token）&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;词元（Token）&lt;/strong&gt;：是模型处理和理解的&lt;strong&gt;基本文本单位&lt;/strong&gt;。它不是完全等同于一个英文单词或一个汉字。模型在处理前，会先将文本拆分成词元，同时，词元也是计费和衡量模型处理长度的基本单位。&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;英文中，单词 &lt;code&gt;“&lt;/code&gt;unbelievable&lt;code&gt;”&lt;/code&gt; 可能会被拆分成三个词元[“un”, “believe”, “able”]&lt;/p&gt; 
 &lt;p&gt;中文中，“我喜欢编程”这句话，很可能会被拆分成四个词元[“我”, “喜”, “欢”, “编程”]&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;strong&gt;不同模型的分词规则不同，同一个词在不同模型中可能被拆分成不同词元&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;了解了 AI 的基本概念，接下来，我们来看&lt;strong&gt; Spring AI&lt;/strong&gt; 相关内容&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;Spring%20AI%20%E6%98%AF%E4%BB%80%E4%B9%88&quot; name=&quot;Spring%20AI%20%E6%98%AF%E4%BB%80%E4%B9%88&quot;&gt;Spring AI 是什么&lt;/h3&gt; 
&lt;p&gt;Spring AI 是一个&lt;strong&gt;基于 Spring 生态系统&lt;/strong&gt;的&lt;strong&gt;开源人工智能应用框架&lt;/strong&gt;，它的核心目标是&lt;strong&gt;简化 AI 功能在 Java 应用程序中的集成过程&lt;/strong&gt;，让 Java 开发者也能高效地构建生成式 AI 应用&lt;/p&gt; 
&lt;p&gt;官方文档：简介 :: Spring AI 参考文档 - Spring 框架&lt;/p&gt; 
&lt;p&gt;Spring AI 提供了作为&lt;strong&gt;开发 AI 应用基础的抽象&lt;/strong&gt;。这些抽象具有多种实现，可以通过&lt;strong&gt;最少的代码更改轻松实现组件切换&lt;/strong&gt;。&lt;/p&gt; 
&lt;p&gt;Spring AI 提供了一系列强大而实用的功能，使其成为一个功能完备的 AI 应用开发框架：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;1. 统一的多模型支持&lt;/strong&gt;：支持与众多主流的 AI 模型提供商进行交互，包括 &lt;strong&gt;OpenAI&lt;/strong&gt;、&lt;strong&gt;Microsoft&lt;/strong&gt;、&lt;strong&gt;Amazon&lt;/strong&gt;、&lt;strong&gt;Google&lt;/strong&gt; 和 &lt;strong&gt;Anthropic&lt;/strong&gt; 等，无论是云端模型还是本地部署的模型（如通过 Ollama），&lt;strong&gt;都能通过一致的接口进行调用&lt;/strong&gt;&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;2. 强大的数据集成能力&lt;/strong&gt;：这是 Spring AI 的一大亮点。它内置了对&lt;strong&gt;向量数据库&lt;/strong&gt;（如 Chroma、Pinecone、Redis 等）的支持&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;3. 与 Spring 生态无缝集成&lt;/strong&gt;：作为 Spring 大家庭的一员，它能自然地与 &lt;strong&gt;Spring Boot&lt;/strong&gt;、&lt;strong&gt;Spring Data&lt;/strong&gt; 等其他知名项目协同工作&lt;/p&gt; 
 &lt;p&gt;4. &lt;strong&gt;简化的开发模式：&lt;/strong&gt;允许 AI 模型根据需要请求执行客户端定义的函数，从而接入实时信息或触发具体动作&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;了解了相关概念后，我们就来上手体验一下&lt;strong&gt; Spring AI&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8&quot; name=&quot;%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8&quot;&gt;快速入门&lt;/h2&gt; 
&lt;h3 id=&quot;%E7%8E%AF%E5%A2%83%E8%A6%81%E6%B1%82&quot; name=&quot;%E7%8E%AF%E5%A2%83%E8%A6%81%E6%B1%82&quot;&gt;环境要求&lt;/h3&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;JDK 版本：JDK 17 或以上&lt;/strong&gt; (推荐 JDK 21)，这是强制要求，因为 Spring Boot 3.x 本身就需要 JDK 17+&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;Spring Boot 版本：Spring Boot 3.2 或以上&lt;/strong&gt; ，具体版本可以是 3.3.3、3.4.3 或 3.5.0，选择一个稳定的&lt;strong&gt;3.x&lt;/strong&gt;最新版本即可。&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;AI 服务凭证：&lt;/strong&gt;有效的 &lt;strong&gt;API Key，&lt;/strong&gt;需要一个来自 &lt;strong&gt;AI 服务提供商&lt;/strong&gt;（如 OpenAI、DeepSeek、阿里百炼等）的账户和 API Key&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;在本篇文章中，我们以 &lt;strong&gt;DeepSeek &lt;/strong&gt;作为示例来进行学习&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E7%94%B3%E8%AF%B7%20API%20Key&quot; name=&quot;%E7%94%B3%E8%AF%B7%20API%20Key&quot;&gt;申请 API Key&lt;/h3&gt; 
&lt;p&gt;我们访问 DeepSeek 官网：DeepSeek | 深度求索&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;进入 API 开放平台：&lt;/strong&gt;&lt;/p&gt; 
 
&lt;p&gt;&lt;strong&gt;创建 API Key：&lt;/strong&gt;&lt;/p&gt; 
 
&lt;p&gt;点击创建之后输入名称即可完成创建，但需要注意的是 &lt;strong&gt;API key 仅在创建时可见可复制&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;然后需要进行充值（充值前需要进行实名认证）&lt;/strong&gt;：&lt;/p&gt; 
 
&lt;p&gt;只是学习使用，&lt;strong&gt;1块&lt;/strong&gt;就够了&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E9%A1%B9%E7%9B%AE%E5%88%9B%E5%BB%BA&quot; name=&quot;%E9%A1%B9%E7%9B%AE%E5%88%9B%E5%BB%BA&quot;&gt;项目创建&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;Spring AI &lt;/strong&gt;专门为 OpenAI 及兼容 API 服务设计了&lt;strong&gt; spring-ai-openai-spring-boot-starter&lt;/strong&gt;，用于快速集成大模型语言能力到 Spring Boot 应用中：&lt;/p&gt; 
 
&lt;p&gt;正常创建 &lt;strong&gt;Maven &lt;/strong&gt;项目（注意JDK 和 Spring Boot 版本），并&lt;strong&gt;添加 Spring AI 依赖&lt;/strong&gt;：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-XML&quot;&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.ai&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-ai-openai-spring-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.0.0-M6&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;依赖版本可参考：https://docs.spring.io/spring-ai/reference/getting-started.html&lt;/p&gt; 
&lt;p&gt;在 &lt;strong&gt;application.yml &lt;/strong&gt;中配置 API 密钥：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;spring:
  ai:
    openai:
      #DeepSeek
      api-key: 申请的 API Key
      base-url: https://api.deepseek.***
      chat:
        options:
          model: deepseek-chat
          temperature: 0.7&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;其中：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;spring.ai.openai.base-url&lt;/strong&gt;：要连接的 URL&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;spring.ai.openai.api-key&lt;/strong&gt;：申请的 DeepSeek API 密钥&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;spring.ai.openai.chat.options.model&lt;/strong&gt;：要使用的 &lt;strong&gt;DeepSeek LLM 模型&lt;/strong&gt;&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;spring.ai.openai.chat.options.temperature&lt;/strong&gt;：用于&lt;strong&gt;控制模型生成文本的随机性和创造性：低温 (接近 0.0)&lt;/strong&gt; → 保守、确定、可预测；&lt;strong&gt;高温 (接近 2.0) &lt;/strong&gt;→ 冒险、多样、富有想象力。也就是说，&lt;strong&gt;temperature &lt;/strong&gt;值越低，相同的提问得到的结果越类似。&lt;strong&gt;此外，&lt;/strong&gt;不建议在同一个补全请求中同时修改 &lt;strong&gt;temperature &lt;/strong&gt;和 &lt;strong&gt;top_p&lt;/strong&gt;，因为这两个设置的交互作用难以预测。&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;配置项可参考：https://docs.spring.io/spring-ai/reference/api/chat/deepseek-chat.html&lt;/p&gt; 
&lt;p&gt;此时，就已经完成了 &lt;strong&gt;项目的创建 &lt;/strong&gt;和&lt;strong&gt; DeepSeek 的接入&lt;/strong&gt;，接下来，我们通过编写接口来调用模型&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E6%8E%A5%E5%8F%A3%E7%BC%96%E5%86%99&quot; name=&quot;%E6%8E%A5%E5%8F%A3%E7%BC%96%E5%86%99&quot;&gt;接口编写&lt;/h3&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequestMapping(&quot;/deepseek&quot;)
public class DeepSeekChatController {
    @Autowired
    private OpenAiChatModel deepSeekChatModel;

    @GetMapping(&quot;/chat&quot;)
    public String generate(String message) {
        return deepSeekChatModel.call(message);
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;运行，并访问 127.0.0.1:8080/deepseek/chat?message=你是谁 进行测试&lt;/p&gt; 
 
&lt;p&gt;上述，我们通过 &lt;strong&gt;ChatModel &lt;/strong&gt;完成了与模型的交互&lt;/p&gt; 
&lt;p&gt;而在&lt;strong&gt; Spring AI&lt;/strong&gt; 框架中，&lt;strong&gt;ChatModel &lt;/strong&gt;和 &lt;strong&gt;ChatClient &lt;/strong&gt;是构建 对话式AI 应用的两大核心接口，接下来，我们分别来看这两个接口&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E6%A0%B8%E5%BF%83%E6%8E%A5%E5%8F%A3&quot; name=&quot;%E6%A0%B8%E5%BF%83%E6%8E%A5%E5%8F%A3&quot;&gt;核心接口&lt;/h2&gt; 
&lt;h3 id=&quot;ChatModel%C2%A0&quot; name=&quot;ChatModel%C2%A0&quot;&gt;ChatModel &lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;ChatMode &lt;/strong&gt;直接与底层 AI 模型（如 GPT-4、Claude 等）通信，处理原始的请求和响应&lt;/p&gt; 
&lt;p&gt;因此，&lt;strong&gt;ChatModel &lt;/strong&gt;更底层，使用更灵活&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class ChatService {
    
    @Autowired
    private ChatModel chatModel; // 例如 OpenAiChatModel
    
    public String askQuestion(String question) {
        // 1. 构造消息
        UserMessage userMessage = new UserMessage(question);
        
        // 2. 创建 Prompt
        Prompt prompt = new Prompt(List.of(userMessage));
        
        // 3. 调用并获得完整响应
        ChatResponse response = chatModel.call(prompt);
        
        // 4. 从响应中提取内容
        return response.getResult().getOutput().getContent();
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;ChatClient&quot; name=&quot;ChatClient&quot;&gt;ChatClient&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;ChatClient&lt;/strong&gt; 在 &lt;strong&gt;ChatModel &lt;/strong&gt;之上提供了一层&lt;strong&gt;流畅的 API&lt;/strong&gt;，简化了常见的使用模式&lt;/p&gt; 
 
&lt;p&gt;即 &lt;strong&gt;ChatClient&lt;/strong&gt; 是对 &lt;strong&gt;ChatMode &lt;/strong&gt;的一层包装&lt;/p&gt; 
&lt;p&gt;因此，&lt;strong&gt;ChatClient&lt;/strong&gt; 更高级，也更简洁&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class ChatService {
    
    @Autowired
    private ChatClient chatClient;
    
    public String askQuestion(String question) {
        // 一行搞定
        return chatClient.call(question);
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;可以看到，&lt;strong&gt;ChatClient&lt;/strong&gt; 的使用更加简洁直观&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;ChatMode &lt;/strong&gt;与 &lt;strong&gt;ChatClient&lt;/strong&gt; 对比：&lt;/p&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th style=&quot;width:109px&quot;&gt;维度&lt;/th&gt;
&lt;th style=&quot;width:285px&quot;&gt;ChatModel&lt;/th&gt;
&lt;th&gt;ChatClient&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width:109px&quot;&gt;&lt;strong&gt;抽象层级&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:285px&quot;&gt;底层，接近原始模型&lt;/td&gt;
&lt;td&gt;高层，面向业务使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width:109px&quot;&gt;&lt;strong&gt;返回值&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:285px&quot;&gt;ChatResponse（包含丰富元数据的完整响应对象）&lt;/td&gt;
&lt;td&gt;ChatResponse（直接获得内容的纯文本）或流式响应&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width:109px&quot;&gt;&lt;strong&gt;使用方法&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:285px&quot;&gt;需要手动构造 Prompt 对象&lt;/td&gt;
&lt;td&gt;提供流式的 builder 模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width:109px&quot;&gt;&lt;strong&gt;控制粒度&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:285px&quot;&gt;精细控制&lt;/td&gt;
&lt;td&gt;快捷简便&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B&quot; name=&quot;%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B&quot;&gt;消息类型&lt;/h2&gt; 
&lt;p&gt;在 Spring AI 中，所有消息类型都实现了 &lt;strong&gt;org.springframework.ai.chat.messages.Message &lt;/strong&gt;接口，系统中的消息被设计用来&lt;strong&gt;模拟一个多轮对话中的不同参与者&lt;/strong&gt;&lt;/p&gt; 
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;消息类型&lt;/th&gt;
&lt;th style=&quot;width:121px&quot;&gt;对应角色&lt;/th&gt;
&lt;th style=&quot;width:428px&quot;&gt;核心作用&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SystemMessage&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:121px&quot;&gt;&lt;strong&gt;系统 / 导演&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:428px&quot;&gt;设定 AI 的背景、角色、行为和回复风格。通常在对话开始时提供，为整个会话定下基调。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UserMessage&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:121px&quot;&gt;&lt;strong&gt;用户 / 提问者&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:428px&quot;&gt;代表人机交互中的人类一方，是驱动对话前进的源泉。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AssistantMessage&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:121px&quot;&gt;&lt;strong&gt;助理 / AI 本身&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:428px&quot;&gt;代表 AI 在之前轮次中做出的回复。是多轮对话连贯性的保障。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FunctionMessage&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:121px&quot;&gt;&lt;strong&gt;函数 / 工具&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:428px&quot;&gt;代表 AI 通过函数调用获得的额外信息或操作结果。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ToolMessage&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:121px&quot;&gt;&lt;strong&gt;工具&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:428px&quot;&gt;功能与 ToolMessage 完全相同，是 ToolMessage 的别名。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MediaMessage&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:121px&quot;&gt;&lt;strong&gt;多媒体&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;width:428px&quot;&gt;表示除文本外的其他类型消息数据，例如图像。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt; 
&lt;p&gt;其中，最常使用的是 &lt;strong&gt;SystemMessage、UserMessage 和 AssistantMessage&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;SystemMessage&quot; name=&quot;SystemMessage&quot;&gt;&lt;strong&gt;SystemMessage&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;SystemMessage &lt;/strong&gt;通常用于设定 AI 助手的&lt;strong&gt;身份、性格、行为准则和对话规则，一般位&lt;/strong&gt;于对话的开头，为整个对话设定基调&lt;/p&gt; 
&lt;p&gt;例如，我们可以为其进行&lt;strong&gt;角色预设&lt;/strong&gt;：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequestMapping(&quot;/chat&quot;)
@RestController
public class ChatClientController {
    private ChatClient chatClient;
    public ChatClientController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder
                .defaultSystem(&quot;你叫小小鱼，是一款专业的智能答疑AI助手，擅长Java和Python，以友好的态度来回答问题&quot;)
                .build();
    }

    @GetMapping(&quot;/call&quot;)
    public String generation(String userInput) {
        return this.chatClient.prompt()
                .user(userInput) // 用户输入
                .call() // 调用 API
                .content(); // 返回响应
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;在 &lt;strong&gt;ChatClient &lt;/strong&gt;中，通过 &lt;strong&gt;defaultSystem &lt;/strong&gt;来设置 AI 模型的&lt;strong&gt;默认系统消息&lt;/strong&gt;，通过 &lt;strong&gt;ChatClient.Builder &lt;/strong&gt;链式调用设置的系统消息会作为对话的 &quot;&lt;strong&gt;初始指令&lt;/strong&gt;&quot;，注入到每次对话的上下文中，引导 AI 的回复风格或身份设定&lt;/p&gt; 
&lt;p&gt;此时，我们访问接口，再次询问其身份：&lt;/p&gt; 
 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;UserMessage&quot; name=&quot;UserMessage&quot;&gt;UserMessage&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;UserMessage &lt;/strong&gt;表示我们提出的具体问题或指令，上述输入的 &quot;你是谁&quot;，就是 &lt;strong&gt;UserMessage&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;AssistantMessage&quot; name=&quot;AssistantMessage&quot;&gt;AssistantMessage&lt;/h3&gt; 
&lt;p&gt;&lt;strong&gt;AssistentMessage&lt;/strong&gt;：是AI模型给出的回复&lt;/p&gt; 
 
&lt;p&gt;&lt;strong&gt;AssistentMessage &lt;/strong&gt;是实现 &lt;strong&gt;连贯多轮对话 &lt;/strong&gt;的关键。每次 AI 回复后，可以将这个回复作为 &lt;strong&gt;AssistantMessage &lt;/strong&gt;保存下来，并在下一次请求时将其作为历史上下文的一部分发送给 AI&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;%E8%BE%93%E5%87%BA%E6%A0%BC%E5%BC%8F&quot; name=&quot;%E8%BE%93%E5%87%BA%E6%A0%BC%E5%BC%8F&quot;&gt;输出格式&lt;/h2&gt; 
&lt;h3 id=&quot;%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BA&quot; name=&quot;%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BA&quot;&gt;结构化输出&lt;/h3&gt; 
&lt;p&gt;若想要从 LLM 接收&lt;strong&gt;结构化输出&lt;/strong&gt;，Spring AI 支持将&lt;strong&gt; ChatModel/ChatClient &lt;/strong&gt;方法的返回类型从 Spring 更改为其他类型&lt;/p&gt; 
&lt;p&gt;通过&lt;strong&gt; entity() &lt;/strong&gt;方法将模型输出转化为&lt;strong&gt;自定义实体&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;例如：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequestMapping(&quot;/chat&quot;)
@RestController
public class ChatClientController { 
    private ChatClient chatClient;
    public ChatClientController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder
                .build();
    }

   @GetMapping(&quot;/entity&quot;)
    public String entity(String userInput) {
        Recipe entity = this.chatClient.prompt()
                .user(String.format(&quot;请帮我生成%s的菜谱&quot;, userInput))
                .call()
                .entity(Recipe.class);
        return entity.toString();
    }

    record Recipe(String dis, List&amp;lt;String&amp;gt; ingredients) {}
}&lt;/code&gt;&lt;/pre&gt; 
 
&lt;p&gt;&lt;/p&gt; 
&lt;h3 id=&quot;%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA&quot; name=&quot;%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA&quot;&gt;流式输出&lt;/h3&gt; 
&lt;p&gt;首先我们对比来看什么是流式输出：&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;传统输出（非流式&lt;/strong&gt;）：&lt;strong&gt;等待全部生成完成&lt;/strong&gt;后才一次性返回，用户长时间等待 → 突然显示完整答案。像&lt;strong&gt;寄送一封平信&lt;/strong&gt;，写完所有内容才寄出，对方一次性收到整封信。&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;流式输出&lt;/strong&gt;：&lt;strong&gt;边生成边返回&lt;/strong&gt;，立即推送部分结果，几乎立即开始显示 → 逐字逐句增长。像&lt;strong&gt;打电话&lt;/strong&gt;一样，对方一边说话，你一边就能听到。&lt;/p&gt; 
&lt;p&gt;流式输出过程：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;用户提问: &quot;请写一篇关于春天的短文&quot;&lt;/p&gt; 
 &lt;p&gt;AI 模型生成过程:&lt;/p&gt; 
 &lt;p&gt;&quot;春天&quot;... (立即返回)&lt;/p&gt; 
 &lt;p&gt;&quot;春天来了&quot;... (继续返回)&lt;/p&gt; 
 &lt;p&gt;&quot;春天来了，万物复苏&quot;... (持续返回)&lt;/p&gt; 
 &lt;p&gt;直到生成完整回答&lt;span style=&quot;color:#4d4d4d&quot;&gt;SSE 协议&lt;/span&gt;&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;Spring AI 主要通过 &lt;strong&gt;响应式编程&lt;/strong&gt; 来实现流式输出，使用 &lt;strong&gt;stream() &lt;/strong&gt;方法生成&lt;strong&gt; Flux&amp;lt;String&amp;gt; &lt;/strong&gt;流&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequestMapping(&quot;/chat&quot;)
@RestController
public class ChatClientController { 
    private ChatClient chatClient;
    public ChatClientController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder
                .build();
    }

    @GetMapping(value = &quot;/stream&quot;, produces = &quot;text/html;charset=utf-8&quot;)
    public Flux&amp;lt;String&amp;gt; stream(String userInput) {
        return this.chatClient.prompt()
                .user(userInput)
                .stream()
                .content();
    }

    record Recipe(String dis, List&amp;lt;String&amp;gt; ingredients) {}
}&lt;/code&gt;&lt;/pre&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;但是，我们思考这样一个问题：由于 HTTP 协议本身设计为无状态的请求-响应模式，也就是严格来说，无法做到服务器主动推送消息到客户端，那么我们要如何实现服务器的流式响应呢？&lt;/strong&gt;&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;我们可以通过 &lt;strong&gt;SSE（Server-Sent Events，服务器发送事件）&lt;/strong&gt;来实现流式传输，允许服务器&lt;strong&gt;主动向浏览器推送数据流&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h4 id=&quot;SSE%20%E5%8D%8F%E8%AE%AE%E4%BB%8B%E7%BB%8D&quot; name=&quot;SSE%20%E5%8D%8F%E8%AE%AE%E4%BB%8B%E7%BB%8D&quot;&gt;SSE 协议介绍&lt;/h4&gt; 
&lt;p&gt;&lt;strong&gt;SSE &lt;/strong&gt;是一种&lt;strong&gt;基于 HTTP &lt;/strong&gt;的&lt;strong&gt;轻量级实时通信协议&lt;/strong&gt;，浏览器通过&lt;strong&gt;内置的 EventSource API &lt;/strong&gt;接收并处理这些实时事件&lt;/p&gt; 
&lt;p&gt;服务器向客户端声明：接下来发送的是 &lt;strong&gt;流消息（streaming）、&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;此时客户端不会关闭连接，会一直等待服务器发送过来新的数据流&lt;/p&gt; 
&lt;p&gt;SSE 核心特点：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;1. 单向通信&lt;/strong&gt;：数据流&lt;strong&gt;只能从服务器推送到客户端&lt;/strong&gt;。客户端不能通过这个连接向服务器发送数据（除了最初的建立连接请求）。&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;2. 基于 HTTP/HTTPS&lt;/strong&gt;：SSE 使用标准的 HTTP 协议，这意味着它可以轻松地穿越大多数防火墙和代理服务器，无需特殊的配置。&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;3. 长连接&lt;/strong&gt;：客户端发起一个普通的 HTTP 请求，但服务器会保持这个连接处于打开状态，而不是在发送一次响应后就关闭它。&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;4. 文本数据流&lt;/strong&gt;：服务器通过这个持久的连接，持续地向客户端发送遵循特定格式的文本数据流。&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;5. 自动重连&lt;/strong&gt;：SSE 协议内建了重连机制。如果连接意外断开，浏览器会自动尝试重新连接到服务器。&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h4 id=&quot;SSE%20%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F&quot; name=&quot;SSE%20%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F&quot;&gt;SSE 数据格式&lt;/h4&gt; 
&lt;p id=&quot;u5bd20dec&quot;&gt;服务器向浏览器发送 SSE 数据，需要设置必须的&lt;strong&gt; HTTP 头信息&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Content-Type: text/event-stream;charset=utf-8
Connection: keep-alive&lt;/code&gt;&lt;/pre&gt; 
&lt;p id=&quot;u023638c6&quot;&gt;整个数据流由&lt;strong&gt;一系列消息&lt;/strong&gt;组成，&lt;strong&gt;每条消息（message）&lt;/strong&gt;由&lt;strong&gt;一行或多行文本&lt;/strong&gt;构成，每行文本以一个&lt;strong&gt;字段名&lt;/strong&gt;开头，后跟一个冒号和一个空格，然后是字段的值，每条消息以一个&lt;strong&gt;空行&lt;/strong&gt;（即两个连续的换行符 \n\n）结束&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;每一行格式：&lt;strong&gt;[field]: value\n&lt;/strong&gt;&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;field 的常见取值有：&lt;strong&gt;data、event、id、retry&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h5 id=&quot;data&quot; name=&quot;data&quot;&gt;data&lt;/h5&gt; 
&lt;p&gt;&lt;strong&gt;data&lt;/strong&gt;：&lt;strong&gt;消息主体，&lt;/strong&gt;是最重要的字段，用于&lt;strong&gt;承载消息的实际内容&lt;/strong&gt;，如果一个消息包含多个 &lt;code&gt;data&lt;/code&gt; 行，客户端会将它们用换行符 (&lt;code&gt;\n&lt;/code&gt;) 连接起来，形成一个完整的数据字符串。可用于传递 JSON 字符串、纯文本、XML 等任何文本数据&lt;/p&gt; 
&lt;p&gt;示例：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;data: 这是一条简单的消息\n\n&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;data: Hello\n&lt;/p&gt; 
 &lt;p&gt;data: World\n&lt;/p&gt; 
 &lt;p&gt;data: !\n\n&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h5 id=&quot;event&quot; name=&quot;event&quot;&gt;event&lt;/h5&gt; 
&lt;p&gt;&lt;strong&gt;event&lt;/strong&gt;：事件类型，用于指定消息的自定义类型，若提供了此字段，客户端将触发对该特定事件名的监听器；否则，将触发通用的 &lt;strong&gt;&lt;code&gt;onmessage&lt;/code&gt; &lt;/strong&gt;事件，可用于&lt;strong&gt;对不同类型的消息进行分类处理&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;示例：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;event: userJoined&lt;br&gt; data: Alice&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h5 id=&quot;id&quot; name=&quot;id&quot;&gt;id&lt;/h5&gt; 
&lt;p&gt;&lt;strong&gt;id&lt;/strong&gt;：事件id，用于为消息设置一个唯一的 ID（字符串），如果连接中断，当客户端重新连接时，会在 HTTP 请求头&lt;strong&gt; &lt;code&gt;Last-Event-ID&lt;/code&gt; &lt;/strong&gt;中自动发送最后一个接收到的 ID。可以用于实现&lt;strong&gt;消息的幂等性和断点续传&lt;/strong&gt;。&lt;/p&gt; 
&lt;p&gt;示例：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;id: msg-123&lt;br&gt; data: 这是一条重要消息&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h5 id=&quot;retry&quot; name=&quot;retry&quot;&gt;retry&lt;/h5&gt; 
&lt;p&gt;&lt;strong&gt;retry&lt;/strong&gt;：重连时间，表示&lt;strong&gt;建议浏览器在连接断开后再次尝试连接之前应等待的毫秒数&lt;/strong&gt;，由于这&lt;strong&gt;不是一个强制命令&lt;/strong&gt;，浏览器&lt;strong&gt;可能会忽略它。&lt;/strong&gt;用于避免在服务器出现故障时，客户端过于频繁地重试。&lt;/p&gt; 
&lt;p&gt;示例：告诉浏览器，如果连接失败，请等待 10 秒后再尝试重连&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;retry: 10000&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;我们通过一个简单的示例来看 SSE 协议的使用&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h4 id=&quot;SSE%20%E5%8D%8F%E8%AE%AE%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B&quot; name=&quot;SSE%20%E5%8D%8F%E8%AE%AE%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B&quot;&gt;SSE 使用示例&lt;/h4&gt; 
&lt;p&gt;&lt;strong&gt;后端接口：&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Slf4j
@RequestMapping(&quot;/sse&quot;)
@RestController
public class SseController {
    @RequestMapping(&quot;/end&quot;)
    public void end(HttpServletResponse response) throws IOException, InterruptedException {
        log.info(&quot;发起请求: event&quot;);
        response.setContentType(&quot;text/event-stream;charset=utf-8&quot;);
        PrintWriter writer = response.getWriter();
        for (int i = 0; i &amp;lt; 10; i++) {
            // 事件 foo 事件
            String s = &quot;event: foo\n&quot;;
            s += &quot;data: &quot; + new Date() + &quot;\n\n&quot;;
            writer.write(s);
            writer.flush();
            Thread.sleep(1000L);
        }
        //定义end事件, 表示当前流传输结束
        writer.write(&quot;event: end\ndata: EOF\n\n&quot;);
        writer.flush();
    }
}&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;strong&gt;前端实现：&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;SSE&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;sse&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;
        let eventSource = new EventSource(&quot;/sse/end&quot;);
        eventSource.addEventListener(&quot;foo&quot;, function(event) {
            console.log(event);
            document.getElementById(&quot;sse&quot;).innerHTML = event.data;
        });
        eventSource.addEventListener(&quot;end&quot;, function(event) {
            console.log(&quot;连接关闭&quot;)
            eventSource.close();
        });
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;运行，观察控制台打印的日志：&lt;/p&gt; 
 
&lt;p&gt;可以看到成功传输消息，并在消息传输完毕后关闭连接&lt;/p&gt; 
&lt;p&gt;而在 Spring 中，可以通过&lt;strong&gt; WebFlux&lt;/strong&gt; 优雅地实现 SSE 协议，也就是我们之前使用的 &lt;strong&gt;Flux&lt;/strong&gt;，它是 &lt;strong&gt;WebFlux &lt;/strong&gt;中的核心组件，我们来看&lt;strong&gt; Flux 的使用和常见操作&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h4 id=&quot;Flux&quot; name=&quot;Flux&quot;&gt;Flux&lt;/h4&gt; 
&lt;p&gt;Flux 的使用流程：&lt;strong&gt;创建 → 转换 → 过滤 → 消费&lt;/strong&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;创建 Flux&lt;/strong&gt;：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import reactor.core.publisher.Flux;

// 1.1 从固定值创建
Flux&amp;lt;String&amp;gt; fixedFlux = Flux.just(&quot;Hello&quot;, &quot;World&quot;, &quot;!&quot;);

// 1.2 从集合创建
List&amp;lt;String&amp;gt; list = Arrays.asList(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;);
Flux&amp;lt;String&amp;gt; fromCollection = Flux.fromIterable(list);

// 1.3 数值范围
Flux&amp;lt;Integer&amp;gt; rangeFlux = Flux.range(1, 5); // 1,2,3,4,5

// 1.4 动态生成
Flux&amp;lt;Long&amp;gt; intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(5);

// 1.5 从数组创建
Flux&amp;lt;String&amp;gt; arrayFlux = Flux.fromArray(new String[]{&quot;X&quot;, &quot;Y&quot;, &quot;Z&quot;}));

// 1.6 空流
Flux&amp;lt;String&amp;gt; emptyFlux = Flux.empty();

// 1.7 从 Future 创建（适配传统异步API）
***pletableFuture&amp;lt;String&amp;gt; future = ***pletableFuture.supplyAsync(() -&amp;gt; &quot;Result&quot;);

Flux&amp;lt;String&amp;gt; futureFlux = Flux.fromFuture(future);&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;strong&gt;转化操作&lt;/strong&gt;：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 2.1 map - 一对一转换
Flux&amp;lt;String&amp;gt; original = Flux.just(&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;);
Flux&amp;lt;String&amp;gt; uppercased = original.map(String::toUpperCase); // APPLE, BANANA, CHERRY

// 2.2 flatMap - 一对多转换（异步展平）
Flux&amp;lt;String&amp;gt; words = Flux.just(&quot;hello world&quot;, &quot;spring ai&quot;);
Flux&amp;lt;String&amp;gt; splitWords = words.flatMap(word -&amp;gt; 
    Flux.fromArray(word.split(&quot; &quot;))
);

// 2.3 cast - 类型转换
Flux&amp;lt;Object&amp;gt; objects = Flux.just(&quot;text1&quot;, &quot;text2&quot;);
Flux&amp;lt;String&amp;gt; strings = objects.cast(String.class);

// 2.4 scan - 累积计算
Flux&amp;lt;Integer&amp;gt; numbers = Flux.range(1, 4);
Flux&amp;lt;Integer&amp;gt; cumulativeSum = numbers.scan((a***, current) -&amp;gt; a*** + current); // 1,3,6,10&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;strong&gt;过滤操作：&lt;/strong&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 3.1 filter - 条件过滤
Flux&amp;lt;Integer&amp;gt; allNumbers = Flux.range(1, 10);
Flux&amp;lt;Integer&amp;gt; evenNumbers = allNumbers.filter(n -&amp;gt; n % 2 == 0); // 2,4,6,8,10

// 3.2 distinct - 去重
Flux&amp;lt;String&amp;gt; withDuplicates = Flux.just(&quot;A&quot;, &quot;B&quot;, &quot;A&quot;, &quot;C&quot;);
Flux&amp;lt;String&amp;gt; uniqueItems = withDuplicates.distinct()); // A,B,C

// 3.3 take - 取前 N 个
Flux&amp;lt;String&amp;gt; limited = original.take(2); // apple, banana

// 3.4 skip - 跳过前 N 个
Flux&amp;lt;String&amp;gt; skipped = original.skip(1); // banana, cherry

// 3.5 takeWhile / skipWhile - 条件取/跳
Flux&amp;lt;Integer&amp;gt; sequence = Flux.range(1, 100);
Flux&amp;lt;Integer&amp;gt; firstPart = sequence.takeWhile(n -&amp;gt; n &amp;lt; 10); // 1,2,3,...,9

// 3.6 sample - 采样（定期取最新元素）
Flux&amp;lt;Long&amp;gt; sampled = Flux.interval(Duration.ofMillis(100)))
        .sample(Duration.ofSeconds(1)))
        .take(3); // 每隔1秒取样，共取3次&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;strong&gt;消费操作&lt;/strong&gt;：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 4.1 subscribe - 最基本的消费方式
Flux&amp;lt;String&amp;gt; data = Flux.just(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;);
data.subscribe(
    item -&amp;gt; System.out.println(&quot;Received: &quot; + item),
    error -&amp;gt; System.err.println(&quot;Error: &quot; + error)),
   ,
    () -&amp;gt; System.out.println(&quot;***pleted!&quot;))
);

// 4.2 collectList - 收集所有元素到 List
Mono&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; listMono = data.collectList());

// 4.3 blockFirst / blockLast - 阻塞获取（仅用于测试）
// String first = data.blockFirst();

// 4.4 reduce - 归约操作
Mono&amp;lt;Integer&amp;gt; sum = numbers.reduce(0, Integer::sum));

// 4.5 count - 计数
Mono&amp;lt;Long&amp;gt; count = data.count();

// 4.6 hasElement - 检查是否有元素
Mono&amp;lt;Boolean&amp;gt; hasData = data.hasElements();

// 4.7 then - 忽略元素，只在完成后触发
Mono&amp;lt;Void&amp;gt; ***pletionSignal = data.then();&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;/p&gt; 
&lt;h2 id=&quot;Advisors&quot; name=&quot;Advisors&quot;&gt;Advisors&lt;/h2&gt; 
&lt;p&gt;&lt;strong&gt;Advisors 是 Spring AI 中的一种拦截器机制&lt;/strong&gt;，允许我们在 AI 调用链的特定节点注入自定义逻辑。&lt;/p&gt; 
&lt;p&gt;Advisors 在两个关键的时机点介入：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;strong&gt;Before Call（调用前）&lt;/strong&gt;：在请求发送到 AI 模型&lt;strong&gt;之前&lt;/strong&gt;执行，主要用于&lt;strong&gt;修改提示词&lt;/strong&gt;&lt;/p&gt; 
 &lt;p&gt;&lt;strong&gt;After Call（调用后）：&lt;/strong&gt;在收到 AI 响应后、返回给客户端&lt;strong&gt;之前&lt;/strong&gt;执行&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;其执行流程为：&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;用户输入 → Advisor1.before() → Advisor2.before() → AI 模型调用 → Advisor2.after() → Advisor1.after() → 最终响应&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p&gt;在 Spring AI 中&lt;strong&gt;内置了一些 Advisor&lt;/strong&gt;，如 &lt;strong&gt;SimpleLoggerAdvisor&lt;/strong&gt;，其主要功能是进行日志记录，只需要将其添加到 Advisor 链中，就可以自动记录 Advisor 的聊天请求和响应：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequestMapping(&quot;/chat&quot;)
@RestController
public class ChatClientController { 
    private ChatClient chatClient;
    public ChatClientController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder
                .defaultSystem(&quot;你叫小小鱼，是一款专业的智能答疑AI助手，擅长Java和Python，以友好的态度来回答问题&quot;)
                .build();
    }

    @GetMapping(&quot;/advisor&quot;)
    public String advisor(String userInput) {
        return this.chatClient.prompt()
                .advisors(new SimpleLoggerAdvisor())
                .user(userInput)
                .call()
                .content();
    }

    record Recipe(String dis, List&amp;lt;String&amp;gt; ingredients) {}
}&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;我们将日志级别配置为 &lt;strong&gt;debug &lt;/strong&gt;来观察：&lt;/p&gt; 
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;logging:
  level:
    org.springframework.ai.chat.client.advisor: debug&lt;/code&gt;&lt;/pre&gt; 
 
&lt;p&gt;观察打印的日志内容：&lt;/p&gt; 
 
&lt;p&gt;成功打印了对应的请求和响应日志&lt;/p&gt;</description><pubDate>Wed, 26 Nov 2025 17:21:43 +0800</pubDate></item><item><title>清晰易懂的 PHP 安装与配置教程</title><link>https://ajaa.cn/863.html</link><description>&lt;p&gt;&lt;img src=&quot;https://ajaa.cn/zb_users/upload/2025/11/20251126172143176414890323607.jpg&quot; alt=&quot;清晰易懂的 PHP 安装与配置教程&quot; title=&quot;清晰易懂的 PHP 安装与配置教程&quot; /&gt;&lt;/p&gt;&lt;h2&gt;
初学者也能看懂的 PHP 安装与配置教程&lt;/h2&gt; 
&lt;p&gt;本教程将手把手教你如何在 Windows 系统上安装 PHP，并配置 ***poser（PHP 的依赖管理工具）的缓存位置，即使你是零基础小白，也能轻松完成！&lt;/p&gt; 
&lt;hr&gt; 
&lt;h3&gt;
一、准备工作&lt;/h3&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;操作系统&lt;/strong&gt;：Windows 10/11。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;下载工具&lt;/strong&gt;：浏览器（推荐 Chrome 或 Edge）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;存储空间&lt;/strong&gt;：至少预留 200MB 可用空间。&lt;/li&gt;
&lt;/ol&gt; 
&lt;hr&gt; 
&lt;h3&gt;
二、安装 PHP&lt;/h3&gt; 
&lt;h4&gt;
1. 下载 PHP&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;访问 PHP 官网下载页面：https://windows.php.***/download
&lt;/li&gt;
&lt;li&gt;选择适合你系统的版本： 
  &lt;ul&gt;
&lt;li&gt;推荐下载 &lt;strong&gt;Non Thread Safe (NTS)&lt;/strong&gt; 版本（如 &lt;code&gt;php-8.2.10-nts-Win32-vs16-x64.zip&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;如果你的系统是 32 位，选择 &lt;code&gt;x86&lt;/code&gt; 版本。&lt;/li&gt;
&lt;/ul&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;
2. 解压 PHP&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;将下载的 ZIP 文件解压到一个目录（如 &lt;code&gt;D:\PHP&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;解压后，你会看到以下文件： 
  &lt;ul&gt;
&lt;li&gt;
&lt;code&gt;php.exe&lt;/code&gt;：PHP 解释器。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;php.ini-development&lt;/code&gt;：PHP 配置文件模板。&lt;/li&gt;
&lt;/ul&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;
3. 配置 PHP&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;php.ini-development&lt;/code&gt; 文件重命名为 &lt;code&gt;php.ini&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;打开 &lt;code&gt;php.ini&lt;/code&gt;，找到以下配置项并修改： 
  &lt;ul&gt;
&lt;li&gt;启用扩展（去掉前面的分号 &lt;code&gt;;&lt;/code&gt;）：&lt;pre&gt;&lt;code class=&quot;prism language-ini&quot;&gt;extension_dir = &quot;ext&quot;
extension=curl
extension=gd
extension=mbstring
extension=mysqli
extension=pdo_mysql
extension=openssl
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;设置时区：&lt;pre&gt;&lt;code class=&quot;prism language-ini&quot;&gt;date.timezone = Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ul&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;
4. 配置环境变量&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;右键“此电脑” → 属性 → 高级系统设置 → 环境变量。&lt;/li&gt;
&lt;li&gt;在“系统变量”中找到 &lt;code&gt;Path&lt;/code&gt;，点击“编辑”。&lt;/li&gt;
&lt;li&gt;点击“新建”，输入 PHP 的安装路径（如 &lt;code&gt;D:\PHP&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;点击“确定”保存。&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;
5. 验证安装&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;打开命令提示符（&lt;code&gt;Win + R&lt;/code&gt; → 输入 &lt;code&gt;cmd&lt;/code&gt; → 回车）。&lt;/li&gt;
&lt;li&gt;输入以下命令：&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;php &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;# 输出示例：PHP 8.2.10 (cli) (built: Aug 29 2023 12:00:00)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;hr&gt; 
&lt;h3&gt;
三、安装 ***poser（PHP 依赖管理工具）&lt;/h3&gt; 
&lt;h4&gt;
1. 下载 ***poser&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;访问 ***poser 官网：https://get***poser.org/download
&lt;/li&gt;
&lt;li&gt;下载 Windows 安装程序（&lt;code&gt;***poser-Setup.exe&lt;/code&gt;）。&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;
2. 安装 ***poser&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;双击 &lt;code&gt;***poser-Setup.exe&lt;/code&gt;，启动安装程序。&lt;/li&gt;
&lt;li&gt;选择 PHP 路径（如 &lt;code&gt;D:\PHP\php.exe&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;勾选“Add ***poser to your PATH”，点击“Install”。&lt;/li&gt;
&lt;li&gt;完成安装后，点击“Finish”。&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;
3. 验证 ***poser&lt;/h4&gt; 
&lt;ol&gt;&lt;li&gt;打开命令提示符，输入以下命令：&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;***poser&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;
&lt;span class=&quot;token ***ment&quot;&gt;# 输出示例：***poser version 2.6.5 2023-10-06 10:11:52&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;&lt;/ol&gt; 
&lt;hr&gt; 
&lt;h3&gt;
四、配置 ***poser 缓存位置&lt;/h3&gt; 
&lt;p&gt;默认情况下，***poser 会将下载的依赖包缓存到 &lt;code&gt;C:\Users\&amp;lt;你的用户名&amp;gt;\AppData\Local\***poser&lt;/code&gt;。如果你想将缓存位置改为其他目录，可以按照以下步骤操作：&lt;/p&gt; 
&lt;h4&gt;
1. 设置环境变量&lt;/h4&gt; 
&lt;ol&gt;
&lt;li&gt;右键“此电脑” → 属性 → 高级系统设置 → 环境变量。&lt;/li&gt;
&lt;li&gt;在“系统变量”中点击“新建”，输入： 
  &lt;ul&gt;
&lt;li&gt;变量名：&lt;code&gt;***POSER_CACHE_DIR&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;变量值：自定义路径（如 &lt;code&gt;D:\***poserCache&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt;点击“确定”保存。&lt;/li&gt;
&lt;/ol&gt; 
&lt;h4&gt;
2. 验证缓存路径&lt;/h4&gt; 
&lt;ol&gt;&lt;li&gt;打开命令提示符，输入以下命令：&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;***poser&lt;/span&gt; config &lt;span class=&quot;token parameter variable&quot;&gt;--global&lt;/span&gt; cache-dir
&lt;span class=&quot;token ***ment&quot;&gt;# 输出示例：D:\***poserCache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;&lt;/ol&gt; 
&lt;hr&gt; 
&lt;h3&gt;
五、编写并运行第一个 PHP 程序&lt;/h3&gt; 
&lt;ol&gt;
&lt;li&gt;创建文件 &lt;code&gt;hello.php&lt;/code&gt;，输入以下代码：&lt;pre&gt;&lt;code class=&quot;prism language-php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;你好，世界！&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;li&gt;运行程序： 
  &lt;ul&gt;
&lt;li&gt;打开命令提示符，进入文件所在目录（如 &lt;code&gt;cd D:\PHP&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;输入以下命令：&lt;pre&gt;&lt;code class=&quot;prism language-bash&quot;&gt;php hello.php
&lt;span class=&quot;token ***ment&quot;&gt;# 输出：你好，世界！&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; &lt;/li&gt;
&lt;/ul&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;hr&gt; 
&lt;h3&gt;
六、常见问题&lt;/h3&gt; 
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;php&lt;/code&gt; 命令无效&lt;/strong&gt;： 
  &lt;ul&gt;&lt;li&gt;检查是否将 PHP 安装路径添加到环境变量 &lt;code&gt;Path&lt;/code&gt; 中。&lt;/li&gt;&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;***poser 安装失败&lt;/strong&gt;： 
  &lt;ul&gt;&lt;li&gt;确保 PHP 已正确安装，并且 &lt;code&gt;php.ini&lt;/code&gt; 中启用了 &lt;code&gt;openssl&lt;/code&gt; 扩展。&lt;/li&gt;&lt;/ul&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;缓存路径不生效&lt;/strong&gt;： 
  &lt;ul&gt;&lt;li&gt;确保环境变量 &lt;code&gt;***POSER_CACHE_DIR&lt;/code&gt; 已正确设置，并重启命令提示符。&lt;/li&gt;&lt;/ul&gt; &lt;/li&gt;
&lt;/ol&gt; 
&lt;hr&gt; 
&lt;h3&gt;
七、总结&lt;/h3&gt; 
&lt;p&gt;通过本教程，你已成功完成以下操作：&lt;/p&gt; 
&lt;ol&gt;
&lt;li&gt;安装 PHP 并验证环境。&lt;/li&gt;
&lt;li&gt;安装 ***poser 并配置缓存位置（解放 C 盘空间）。&lt;/li&gt;
&lt;li&gt;编写并运行第一个 PHP 程序。&lt;/li&gt;
&lt;/ol&gt; 
&lt;p&gt;接下来可以学习：&lt;/p&gt; 
&lt;ul&gt;
&lt;li&gt;使用 ***poser 安装第三方库（如 &lt;code&gt;laravel/framework&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;探索 PHP 基础语法和 Web 开发（如 Laravel 框架）。&lt;/li&gt;
&lt;/ul&gt; 
&lt;p&gt;遇到问题欢迎留言讨论，祝你早日成为 PHP 高手！ 🐘🚀&lt;/p&gt;</description><pubDate>Wed, 26 Nov 2025 17:21:41 +0800</pubDate></item></channel></rss>