Skip to content

2026-09 笔记

1. 存储单位与大模型参数量单位

1.1 存储单位(内存、显存、磁盘,字节 Byte)

描述文件、模型权重、数据集占用空间。

二进制(计算机底层):1024 进位;硬盘厂商十进制:1000 进位。

符号全称二进制值(内存 / 显存)十进制(硬盘厂商标注)
BByte 字节1 字节1 字节
KBKilobyte1024 B1000 B
MBMegabyte1024² B1000² B
GBGigabyte1024³ B1000³ B
TBTerabyte1024⁴ B1000⁴ B
PBPetabyte1024⁵ B1000⁵ B

实操:

  • 显存、内存:用 1024;
  • 硬盘、云盘标称 1TB = 1000GB;
  • 模型权重文件大小看:参数量 × 精度。 例:7B FP16 → ~14GB;7B-4bit → ~3.5GB。

1.2 大模型参数量单位

主流就是 M / B / T(Million、Billion、Trillion),工业界几乎只用这三个。

M = 百万,B = 十亿,T = 万亿

有没有更大的?

  • P(Peta):10¹⁵,千万亿参数。

    目前现实世界没有公开 P 级参数的大模型,只存在论文设想里。

    1P params = 1000T 参数

符号英文数值说明
MMillion10⁶ 百万小模型,几百 M 参数
BBillion10⁹ 十亿主流,7B/14B/70B
TTrillion10¹² 万亿超大规模模型
PPeta10¹⁵ 千万亿仅理论,无实际商用模型

再往上 E、Z、Y 这些,完全不会出现在大模型参数场景,不用管。

换算示例(参数数量 → 中文数量级)

text
117M = 117百万 = 1.17亿参数
70B  = 70十亿 = 700亿参数
175B = 175十亿 = 1750亿参数
235B = 235十亿 = 2350亿参数
1.8T = 1.8万亿 = 1万8千亿参数

2. 语法对比:Go := vs Python 海象运算符 :=

时间线

  • Go 的 :=(短变量声明):Go 2009 年公开,:= 的含义是同时声明 + 初始化新变量,只能出现在语句位置,不是表达式,不能用在 if 条件里面。

    go
    // Go: 只能语句,不能写 if (a := fn()) {}
    a := getVal()
  • Python 海象运算符 :=(赋值表达式,PEP-572):2018 提案,Python3.8 正式发布;含义是在表达式内部完成赋值,同时返回值,专门用来写在 if/while 条件里面。

    python
    # Python 海象:允许写在 if 条件内部
    if val := errors.get(first_key):
        msg = val[0]

⚠️ 核心语义差异(非常容易搞混)

项目Go :=Python 海象 :=
正式名称短变量声明赋值表达式(海象运算符)
本质声明语句表达式(有返回值)
能不能用在 if 条件❌ 不可以✅ 可以
作用创建新变量,类型推导给已有 / 新变量赋值,返回赋值结果

3. Python 表单库:wtforms 与 flask-wtf

WTF 全称:WT-Forms → Web Task Forms(Web 任务表单),flask-wtf 是它针对 Flask 的封装库。

注意:不是 what the fuck,是 Web Task Forms。

两者关系

  1. wtforms:底层核心库,框架无关,纯 Python 表单校验、渲染逻辑,可以用在任何 Python 项目(Django、Tornado、原生 Python 都能用),不依赖 Flask。
  2. flask-wtf:对 wtforms 的 Flask 专用扩展,基于 wtforms,做了 Flask 适配,增加 Flask 专属能力。

核心对比

项目wtformsflask-wtf
依赖无 web 框架依赖依赖 wtforms + flask
CSRF 保护没有✅ 内置 CSRF 令牌(Flask session)
表单提交手动拿 request 数据form.validate_on_submit() 一键封装 request
文件上传基础文件字段封装 FileField,集成 Flask 上传校验
Recaptcha 验证码自带 reCAPTCHA 字段
导入写法from wtforms import Form, StringFieldfrom flask_wtf import FlaskForm(继承 FlaskForm 而不是 wtforms.Form

4. Python 依赖注入(injector)

4.1 最简自动注入(无 Module)

只要加 @inject + 类型注解,injector 自动构造依赖树。

python
from injector import Injector, inject

class Database:
    def query(self):
        return "db data"

class UserService:
    @inject
    def __init__(self, db: Database):
        self.db = db

injector = Injector()
service = injector.get(UserService)
print(service.db.query())

4.2 dataclass 写法

python
from dataclasses import dataclass
from injector import Injector, inject

class Database:
    def query(self):
        return "db data"

@inject
@dataclass
class UserService:
    db: Database

injector = Injector()
svc = injector.get(UserService)

4.3 问题:injector.get(UserService) 做了什么?

是的,差不多,但不是"包装 UserService",是:容器帮你完整实例化 UserService,递归把它所有依赖全部自动填好。

看这段:

python
class UserService:
    @inject
    def __init__(self, db: DBConn):
        self.db = db

inj = Injector([AppModule()])
svc = inj.get(UserService)

手动写等价代码(injector 内部帮你干的活):

python
# 框架内部逻辑伪代码
db_conn = inj.get(DBConn)      # 先拿到 DBConn 实例
svc = UserService(db=db_conn)  # 自动把依赖传进去
  • inj.get(X):向容器索要 X 的实例
  • 框架读取 UserService.__init__ 的类型注解,发现需要 db: DBConn
  • 容器再去拿 DBConn 的实例,传给构造函数
  • 你自己不用写 UserService(db=xxx),全部由 DI 容器组装。

关键点:@inject 告诉 injector:这个构造函数的参数,交给容器来提供。没有 @inject,它不会自动填参数。

4.4 Module 到底是干嘛的

Module 是 injector 库的绑定配置容器。

核心作用:集中管理「抽象类 → 具体实现」的映射关系,统一告诉容器:遇到这个抽象接口,该实例化哪个具体类。

看你的例子:

  • IDB 是抽象接口;UserService 的构造只依赖抽象 IDB,完全不知道有 RealDB / TestDB。
  • 如果没有 Module:容器不知道 IDB 该给哪个实现,直接 injector.get(UserService) 会直接报错,因为它不知道 IDB 要实例化成 RealDB 还是 TestDB。
  • Module 里面的 configure(binder),就是写绑定规则的地方。
python
class AppModule(Module):
    def configure(self, binder):
        binder.bind(IDB, to=RealDB)  # 配置:IDB → RealDB

当你创建容器:

python
injector = Injector([AppModule()])

容器会自动执行 AppModule().configure(binder),把所有绑定规则注册进容器。

Module 带来的好处

  1. 解耦切换实现:生产用 AppModule 绑定 RealDB;单元测试换 TestModule 绑定 TestDB。UserService 业务代码一行不动。
  2. 集中管理依赖绑定:项目大了,几十上百个接口绑定,全部收拢到各个 Module,不要散落在业务代码。
  3. 可以多个 Module 传给 Injector([M1(), M2()]),模块可以拆分。

没有 Module 能不能绑定?可以,Injector 第二个参数接收 binder 回调:

python
inj = Injector([], lambda binder: binder.bind(IDB, to=RealDB))

但是项目大了,写 lambda 很乱,所以正式项目都封装成 Module 类。

4.5 binder 的语法是固定的吗?

binder 是 configure 方法传入的 Binder 对象实例,方法名是库固定的 API,不能自己随便改;但是调用方式可以灵活。

binder 核心常用 API(固定)

python
# 1、抽象接口绑定到实现类(你例子用的)
binder.bind(抽象类, to=实现类)

# 2、绑定单例 scope
from injector import singleton
binder.bind(IDB, to=RealDB, scope=singleton)

# 3、直接绑定一个已经实例化好的对象(预先造好的实例)
db_obj = RealDB()
binder.bind(IDB, to=db_obj)

# 4、provider:用一个函数来生成实例(复杂对象初始化)
def create_real_db():
    return RealDB()
binder.bind(IDB, provider=create_real_db)

⚠️ 注意:configure(self, binder) 这个函数签名是固定的,Module 子类必须实现这个方法,框架会自动调用,传入 binder 对象。

两种写法对比

写法 A:继承 Module,重写 configure(工程标准写法,你的示例)

python
class AppModule(Module):
    def configure(self, binder):
        binder.bind(IDB, to=RealDB)

写法 B:不继承 Module,直接传回调,适合小脚本

python
def configure_binder(binder):
    binder.bind(IDB, to=RealDB)

injector = Injector([configure_binder])

可以传函数,不一定非要继承 Module 类。Module 本质就是一个方便封装的语法糖。

4.6 把整个流程串一遍,理解发生了什么

python
injector = Injector([AppModule()])
  1. Injector 初始化,遍历传入的模块列表 [AppModule()]
  2. 如果是 Module 实例,调用它的 .configure(binder),把 binder 对象传进去
  3. 执行 binder.bind(IDB, to=RealDB),容器内部保存映射关系:IDB → RealDB
  4. 后续执行 injector.get(UserService)
    • UserService 构造需要 db: IDB
    • 容器查绑定表:IDB 对应 RealDB → 实例化 RealDB,注入进去。

如果没有绑定:容器看到依赖 IDB 抽象类,不知道该 new 哪个类,直接抛异常。

5. SQLAlchemy ORM 更新与删除

5.1 更新两种方式对比

python
# 方式1: Query.update() 批量更新(直接 SQL,不走 session 缓存)
Students.query.filter_by(name='yy').update({"fullname": "xx"})
# ⚠️ 必须 commit,否则数据库不生效!

# 方式2: 修改实例属性,ORM 对象更新
student = Student.query.first()
student.name = "imooc"
db.session.commit()
特性query.filter().update()修改实例属性
操作对象直接生成 UPDATE SQL,操作数据库修改 session 里的 ORM 对象状态
是否查询不查询数据,直接执行 SQL先把数据查出来到内存
批量✅ 支持批量改多条❗ 只能改已经拿到内存的对象
Session 缓存不会更新 session 缓存里旧对象session 自动跟踪对象变更
需要 commit✅ 必须 commit✅ 必须 commit

坑点:用 .update() 之后,内存中已经查询出来的旧对象不会自动刷新,会出现内存对象和数据库不一致,需要 db.session.refresh(obj) 刷新。

5.2 删除两种方式对比

python
# 方式1: Query.delete() 批量删除
Student.query.filter_by(name="imooc").delete()

# 方式2: db.session.delete(实例)
student = Student.query.first()
db.session.delete(student)
db.session.commit()
特性query.filter().delete()db.session.delete(obj)
原理直接执行 DELETE SQL标记 session 内对象为待删除
查询数据库❌ 不查询✅ 先查询得到对象
级联删除不会触发 ORM 的 cascade 级联逻辑会触发模型定义的级联删除
session 缓存内存旧对象不会清理session 标记对象删除
需要 commit✅ 必须 commit✅ 必须 commit

重点纠正 PPT 的错误原话

"只要使用了 db.session 进行操作都需要调用 commit() 才会将数据同步到实际数据库中,如果使用 ORM 模型执行的操作则不需要。"

❌ 这句话完全错误。

所有写操作:update()delete()、修改实例属性,全部都依赖 session 事务,都要调用 db.session.commit() 才真正写入数据库。

6. Python 上下文管理器(contextlib)

6.1 @contextmanager 装饰器(SQLAlchemy 教程里用到的)

手写 __enter__ / __exit__ 类比较麻烦,contextlib.contextmanager 装饰器,用生成器函数快速写上下文管理器。

规则:

  1. yield 之前的代码 → 等价于 __enter__(进入 with 执行)
  2. yield → 切出去执行 with 缩进里面的业务代码;yield 的返回值就是 as 的变量
  3. yield 之后的代码 → 等价于 __exit__(离开 with,无论报错与否,都会执行)

⚠️ 重点!with 缩进里的业务代码就是运行在 yield 暂停的间隙!

简化演示

python
from contextlib import contextmanager

@contextmanager
def demo():
    print("① enter, 进入 with")
    yield 666     # 暂停,把 666 交给 as 变量
    print("② exit, 离开 with, 一定执行")

with demo() as v:
    print(f"with 内部, v={v}")
    # 1/0 #就算这里报错,依然会打印②
print("外部")

输出:

plaintext
① enter, 进入 with
with 内部, v=666
② exit, 离开 with, 一定执行
外部

6.2 回到 auto_commit,拆解执行流程

python
@contextmanager
def auto_commit(self):
    try:
        yield                  # ← 暂停,切换执行 with 块内数据库业务代码
        self.session.commit()  # ← with 块跑完,回来执行 commit
    except Exception as e:
        self.session.rollback()
        raise e

# 使用
with db.auto_commit():
    student = Student.query.first()
    student.name = "imooc"

执行时序:

  1. 调用 db.auto_commit(),进入函数,走到 yield,暂停
  2. 执行 with 缩进:查询、修改对象
  3. with 块执行完毕,回到 yield 后面,运行 self.session.commit()
  4. 如果 with 内部抛出异常,直接跳入 except 分支,执行 rollback,重新抛出异常

教程 PPT 文字错误回顾:

"我们可以在 yield 之前对数据进行操作(新增、修改、删除)"

❌ 错!业务代码是写在 with 缩进块,运行在 yield 暂停期间,不是写在 yield 前面。yield 前面是初始化阶段。

7. 大模型训练:预训练与后训练

核心对比表

维度预训练 Pre-training后训练 Post-training
核心作用获取知识,决定能力天花板对齐人类,决定能力怎么表现出来
数据特点海量无标注原始文本少量高质量指令、偏好标注数据
训练任务预测下一个 token指令跟随、人类偏好对齐
算力成本极高轻量很多
产出基座 Base 模型可直接对话的 Chat 模型
能不能新增知识主要在这里注入知识几乎不新增知识,只改变输出行为

完整流程链路

plaintext
海量原始文本

【预训练】→ 基座模型(只会续写,不会聊天)

【后训练】
    ├── SFT 监督微调
    └── RLHF / DPO 偏好优化

可直接对外使用的对话大模型

重要误区:后训练几乎不会给模型增加新知识,只是改变它怎么输出答案;模型懂多少,主要是预训练阶段决定的。

8. LangChain 中的 Chain

8.1 Chain 基本介绍

在许多编程语言和库中,Chain 通常用来描述一系列的操作或函数,这些操作或函数按照特定的顺序依次执行,前一个操作的输出会作为后一个操作的输入。这种模式也被称为管道(Pipeline)或链式调用(Chain Calling)。

而在 LangChain 库中,也有 Chain 的概念,用于在复杂场景下,将 LLM 组件、提示词模板、向量存储、记忆、输出解析器等多个组件串联起来一起使用,在 LCEL 表达式出现之前,LangChain 就为 Chain 设计了相应的接口,并且为不同场景设计封装了大量 Chain 组件

在 LangChain 中,存在两种类型的链:

  1. [推荐] 使用 LCEL 构建的链(顺序可执行链);
  2. [遗产] 通过 Chain 类子构建的链,这些链不使用 LCEL,而是独立的类。

例如下方是 0.1.0 版本之前的 Chain 基类的定义:

python
class Chain(BaseModel, ABC):
    """基础接口定义,所有任务 Chain 都会实现这些接口"""

    memory: BaseMemory
    callbacks: Callbacks

    def __call__(
        self,
        inputs: Any,
        return_only_outputs: bool = False,
        callbacks: Callbacks = None,
    ) -> Dict[str, Any]:
        ...

而在 0.1.0 版本之后,Chain 基类也继承了 RunnableSerializable,同样也是一个可运行组件,在使用上了 LCEL 表达式构建的链一模一样,屏蔽了使用的差异,不过在后续的更新中可能会做破坏性的调整。

8.2 内置的 Chain

虽然目前推荐使用 LCEL 表达式的方式来创建链应用,传统链会从 0.3.0 开始逐渐淘汰,但是在 LangChain 封装的 Chain 组件中,但然有绝大部分是基于传统链,有不小的参考价值。

LangChain 内置 Chain 组件文档:https://python.langchain.com/v0.1/docs/modules/chains/

2.1 LCEL Chain

在 LangChain 中,封装的所有 LCEL 链,都是通过函数的方式去调用创建的,通过传递特定的函数,即可快速创建定义好的链应用。

例如 create_stuff_documents_chain 函数会创建一个文档对话链,可以将文档列表作为参数传递给 Prompt,而不需要额外的处理步骤,使用示例如下:

python
import dotenv
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个强大的聊天机器人,能根据对应的上下文信息回复用户问题。\n\n<context>{context}</context>"),
    ("human", "{query}"),
])

llm = ChatOpenAI(model="gpt-3.5-turbo-16k")

chain = create_stuff_documents_chain(llm=llm, prompt=prompt)

9. 向量检索:相似度度量与向量数据库

9.1 余弦相似度与欧式距离

在向量数据库中,支持通过多种方式来计算两个向量的相似度,例如:余弦相似度、欧式距离、曼哈顿距离、闵可夫斯基距离、汉明距离、Jaccard 相似度等多种。其中最常见的就是 余弦相似度 和 欧式距离。

例如下图,左侧就是 欧式距离,右侧就是 余弦相似度。

余弦相似度主要用于衡量向量在方向上的相似性,特别适用于文本、图像和高维空间中的向量。它不受向量长度的影响,只考虑方向的相似程度,余弦相似度的计算公式如下(计算两个向量夹角的余弦值,取值范围为 [-1, 1]):

similarity(A,B)=ABAB=i=1nAiBii=1nAi2i=1nBi2\text{similarity}(\mathbf{A}, \mathbf{B}) = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\|\|\mathbf{B}\|} = \frac{\sum_{i=1}^{n} A_i \cdot B_i}{\sqrt{\sum_{i=1}^{n} A_i^2} \cdot \sqrt{\sum_{i=1}^{n} B_i^2}}

欧式距离衡量向量之间的直线距离,得到的值可能很大,最小为 0,通常用于低维空间或需要考虑向量各个维度之间差异的情况。欧氏距离较小的向量被认为更相似,欧式距离的计算公式如下:

distance(A,B)=i=1n(AiBi)2\text{distance}(\mathbf{A}, \mathbf{B}) = \sqrt{\sum_{i=1}^{n} (A_i - B_i)^2}

9.2 严谨的知识点(考试 / 面试要注意)

  1. Faiss 本质是检索算法库,不是数据库
    • 没有元数据管理、没有集合、没有删除更新、没有并发控制、没有事务,只做向量相似度搜索;元数据、文档内容需要你自己另外保存维护。
    • Milvus、Weaviate、Pinecone 才是真正意义完整向量数据库。
  2. Annoy 同样也是本地库(非服务),原文把 Annoy 放到"本地部署 API 向量数据库"是一个小错误:Annoy 和 Faiss 一样,只是本地库,不提供网络 API 服务,不能通过网络请求访问,不应该跟 Milvus、Weaviate 放一类。

三类整理修正版

分类特征正确例子
本地文件(嵌入式库)无独立服务进程,程序内调用,磁盘存索引文件Faiss、Annoy、Chroma(嵌入式模式)
本地部署 API 向量数据库本地启动独立服务,提供 HTTP/gRPC 网络 APIMilvus、Weaviate、Qdrant
云端 API 向量数据库托管云服务,只调用远程 APIPinecone、TCVectorDB

原文小 bug:Annoy 放错分组;Faiss 在入门教学叫"本地文件向量数据库"方便理解,但记住严谨表述是:向量检索库,不是完整向量数据库

快速记忆

  • Faiss:库,嵌入进程,本地文件保存索引,无网络接口
  • Milvus:服务,独立进程,本地部署,网络 API 访问
  • Pinecone:云托管,完全云端,只调用 API

9.3 常见的切分策略

策略适用场景Chunk大小
按字符数切分通用场景200-500字
按段落切分结构化文档自然段落
按语义切分技术文档完整的概念单元
滑动窗口连续性强的内容重叠50-100字

所以,流程图中的「步骤3:提取相关片段」并不是从完整文档中提取内容,而是从向量数据库中取出检索到的 Chunk 的原始文本。文档在存储时就已经切分好了。

9.4 多路召回(Hybrid Retrieval)

text
+----------------+
|   用户问题      |
+----------------+
        |
  +-----+-----+----------+
  v     v     v          v
向量   关键词  BM25     时间过滤
检索   检索    检索     (最近更新)
  |     |     |          |
  +-----+-----+----------+
        v
    合并 + 去重
        |
        v
   Rerank 重排序
        |
        v
    Top-K 结果

这就是「多路召回」的核心思想:让不同的检索方法各显神通,最后综合决策。就像组建一个专家团队,每个专家擅长不同领域,共同给出最佳答案。

10. 术语读音与词源(anvil / 砧 / Pinecone / Milvus)

10.1 anvil

英 /ˈænvɪl/

名词 n.

  1. 铁砧;砧子(铁匠打铁垫的铁块)

短语:on the anvil 在研讨中;在制作中

10.2 砧

读音:zhēn(第一声)

不要读成 zhàn

释义:

  1. 捶、砸东西时垫在底下的器具:铁砧(anvil)、砧板(切菜板)
  • 砧板:zhēn bǎn,切菜用的板子,很多人误读 zhàn bǎn。

10.3 Pinecone / Milvus 读音、词源释义

Pinecone

音标:英 /ˈpaɪnkəʊn/ 美 /ˈpaɪnkoʊn/

  • 拆分:pine(松树)+ cone(球果)
  • 词义:松果、松塔,就是松树结的松果。
  • 中文近似谐音:派恩扣恩

Pinecone 向量数据库,名字取自松果,寓意存储、索引大量向量。

Milvus

音标:英 /ˈmɪlvəs/,重音在第一音节

  • 词源:来自拉丁语 Milvus,本意是「鸢(一种猛禽,老鹰类)」,国内直接音译 米尔沃斯
  • 中文近似谐音:米尔沃斯

Milvus 是国产开源向量数据库,直接用这个拉丁猛禽单词做项目名,本身日常英文没有这个常用词。

快速对照

名称音标谐音词源含义
Pinecone/ˈpaɪnkoʊn/派恩-扣恩松果
Milvus/ˈmɪlvəs/米尔-沃斯鸢(猛禽)

补充小知识点(面试)

  1. Pinecone:海外主流云原生向量 DB,纯云端托管 SaaS,没有本地部署版本,只提供 API,对应文中「云端 API 向量数据库」。
  2. Milvus:开源,可以本地部署、也可以上云,启动独立服务,提供 gRPC/HTTP API,属于「本地部署 API 向量数据库」。

11. SQLAlchemy 词源与分层

11.1 alchemy 本义:炼金术

al · che · my /ˈælkəmi/ n. 炼金术;魔力转化。

SQL-Alchemy 字面意思:SQL 的炼金术,寓意把 Python 对象「炼成」数据库 SQL,做对象 ↔ 数据库的转换。

11.2 SQLAlchemy 是什么

Python 生态最主流的 ORM 库,就对应前面讲的 ORM。

  • 把 Python 类 / 对象 → 自动翻译成 SQL,操作数据库;
  • 不用手写大量 SQL;
  • 支持 PostgreSQL、MySQL、SQLite 等。

SQLAlchemy 分两大块:

  1. ORM 层(大家最常用):对象关系映射,类映射数据表,User.query() / session 操作,对应 ORM 概念。
  2. Core 层(底层 SQL 抽象):不做对象映射,只是帮你用 Python 语法拼 SQL 语句,返回行数据,属于 SQL 构建器,不用 ORM 也可以单独用 Core。

12. MySQL 索引:DDL 与使用

12.1 创建表时就建立索引

sql
-- 创建表同时建普通索引
CREATE TABLE `user` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  `phone` VARCHAR(20) NOT NULL,
  `age` INT,
  INDEX idx_phone (`phone`),
  INDEX idx_name_age (`name`, `age`),
  UNIQUE INDEX uk_phone (`phone`)
);

PRIMARY KEY 主键本身就是索引(聚簇索引),不需要额外写 INDEX

12.2 表已经存在,事后新增索引(最常用)

sql
-- ✅ 普通索引
CREATE INDEX idx_user_phone ON user(phone);

-- ✅ 联合索引(多字段,顺序很重要,最左前缀原则)
CREATE INDEX idx_name_age ON user(name, age);

-- ✅ 唯一索引:字段不允许重复
CREATE UNIQUE INDEX uk_user_phone ON user(phone);

命名习惯:

  • idx_ 开头:普通索引
  • uk_ 开头:唯一索引

12.3 删除索引

sql
DROP INDEX idx_user_phone ON user;

12.4 查看一张表有哪些索引

sql
SHOW INDEX FROM user;

12.5 验证 SQL 有没有用上索引:explain

select 前面加上 explain,看 type 列:

sql
EXPLAIN SELECT * FROM user WHERE phone = '13800138000';
  • type = ALL:全表扫描,索引失效,没用到
  • type = ref/range:正常命中索引

key 那一列,会显示实际使用的索引名字。

13. 索引原理:最左前缀、回表、覆盖索引

13.1 联合索引:最左前缀原则

索引 idx(a,b,c),能生效的查询条件:

  • where a=?
  • where a=? and b=?
  • where a=? and b=? and c=?
  • where b=?where b=? and c=? 直接失效,跳过最左边 a 就不走索引。

建联合索引,把查询高频、过滤区分度高的字段放最左边。

13.2 覆盖索引(避免回表)

索引 idx_name_age(name, age)

sql
-- ✅ 不需要回表,查询字段全部在索引内
SELECT name, age FROM user WHERE name='张三';

-- ❌ 需要回表,要查不在索引里的 phone
SELECT name, age, phone FROM user WHERE name='张三';

13.3 不要乱建索引

每一条 insert/update/delete,数据库都要维护索引 B+ 树。索引越多写越慢。

13.4 底层:最常见的 B+ 树索引(MySQL InnoDB 默认)

数据表真实数据存在磁盘,是一堆行。索引把索引字段的值拿出来,构建一颗 B+ 树:

  • 树的叶子节点:存放索引值 + 数据行地址 / 主键,按顺序排好;
  • 非叶子节点:只做导航,快速定位到叶子;
  • 叶子节点之间用链表串起来,方便范围查询 > < between

没有索引:查询时数据库一行一行扫描整张表(全表扫描),数据量大巨慢。 有索引:走 B+ 树快速搜索,跳过绝大多数数据。

举例表 user

id (主键)nameagephone
1张三22131xxxx
2李四35132xxxx
3王五28133xxxx

如果给 phone 建立索引:数据库单独构建 B+ 树,树上存一份 phone 值 → 行位置。

sql
select * from user where phone='133xxxx';
  • 无索引:遍历全部行,逐行对比 phone。
  • 有索引:在 B+ 树快速找到 133xxxx,直接定位到对应的那一行。

13.5 什么是回表

前提:只针对 MySQL InnoDB(最常用)

  • 主键索引 = 聚簇索引:B+ 树叶子节点,存放完整的一整行数据。整张表的数据,就放在主键索引树上。
  • 普通二级索引:B+ 树叶子节点,不存完整行,只存【索引字段的值 + 主键 id】。

回表:通过二级索引查到主键 id,再拿着这个 id,去主键索引里面查询拿到完整行数据,这个二次查找动作就叫回表。

举例

user

id (主键)namephone
1张三1310000
2李四1320000

我们给 name 建立普通二级索引 idx_name(name)

二级索引(idx_name)B+ 树叶子存的内容:

plaintext
"张三" → id=1
"李四" → id=2

⚠️ 这里没有 phone 字段!二级索引只存 name 和主键 id。

执行 SQL:

sql
SELECT * FROM user WHERE name = '张三';

完整流程(发生回表)

  1. 在二级索引 idx_name 搜索,找到 "张三",拿到主键 id=1
  2. 二级索引里面只有 name 和 id,没有 phone!拿不到 * 全部字段
  3. 拿着 id=1,去主键聚簇索引 B+ 树,读取完整一行 (id,name,phone) 👈 这一步就是回表
  4. 返回完整数据

回表本质:两次 B+ 树查找。第一次走二级索引,第二次走主键索引。多一次 IO,性能会变差。

13.6 覆盖索引是「查询效果」,不是索引种类

容易混淆的通俗说法

很多教程口语会说「建一个覆盖索引」,这是口语简写。

真实含义是:建一个联合索引,让它能够满足某条 SQL 的全部查询列,使得这条 SQL 可以触发覆盖索引现象,并不是数据库有一种叫「覆盖索引」的新索引。

再回顾 InnoDB 二级索引叶子存储内容

二级索引叶子 = (索引列的值, 主键 id)

  1. 如果 select 取出的字段,全部属于索引列 → 直接从叶子拿数据,不走回表 → 覆盖索引
  2. 如果要查索引以外的列 → 拿主键 id,回表去聚簇索引读取完整行

一句话总结

覆盖索引是查询的效果,不是索引的种类; 底层还是普通 / 联合索引,查询语句刚好可以全部从索引拿到数据,才成为覆盖索引。

13.7 小对比表

名称是索引类型?说明
主键索引(聚簇索引)✅ 是物理真实索引
普通二级索引✅ 是物理真实索引
唯一索引✅ 是物理真实索引
联合索引✅ 是物理真实索引,多字段索引
覆盖索引❌ 不是查询行为、优化现象,依赖上面真实索引实现

补充:Oracle 里面叫索引扫描(index only scan),PostgreSQL 也是叫 index-only-scan,字面意思就是:只扫描索引,不去访问数据表,和 MySQL 覆盖索引是同一个东西。

13.8 使用注意(踩坑点)

  1. 必须类型注解:@inject 靠类型注解解析,无注解无法自动注入。
  2. @singleton 作用域仅限同一个 Injector 实例,新建 Injector 会重新创建对象。
  3. 循环依赖会抛异常,需要重构代码。
  4. 不要全局单例 Injector;一般应用启动构建一次 Injector。
  5. NotBoundError:类型没有绑定,要么自动可实例化,要么在 Module 里显式 bind。

13.9 知识小问答:@inject 到底什么时候发生注入?

问:是不是调用 injector.get(X) 才发生注入?

答:是的,真正做依赖注入、实例创建,发生在 injector.get(SomeClass) 运行时。

@inject 装饰器本身只是打标记,不会立刻做任何实例化。

  • @inject:只是给函数 / 方法对象上贴一个元数据标记,告诉 injector:「这个方法的参数,需要容器来提供」,此时完全不创建对象,不解析依赖。
  • 只有执行 injector.get(Cls) 的时候,容器才:
    1. 读取类的构造函数,看到 @inject 的标记
    2. 读取参数的类型注解
    3. 递归去容器拿到每一个依赖对象
    4. 调用构造函数,把依赖传进去,生成实例

关键点:@inject 告诉 injector —— 这个构造函数的参数,交给容器来提供。没有 @inject,它不会自动填参数。

14. ORM 是什么

ORM (Object-Relational Mapping) 其实是对象映射关系,即将数据库中的表与面向对象编程中的类关联起来:

  • 把数据库中的映射为
  • 表中的映射为类的实例
  • 表中的映射为类的属性

这样一来,就可以通过对类的操作来进行数据库的增删改查,而不必直接操作数据库,让程序员更加专注于业务逻辑,减少了与数据库交互的复杂性。

通过 ORM 可以在代码中少写甚至不写 SQL,去实现数据库的增删改查等复杂业务,同时确保 SQL 安全,不被注入。

从上面可以看出 ORM 的优缺点其实非常明显:

优点

  • 有语法提示,省去自己拼写 SQL,保证 SQL 语法的正确性;
  • ORM 提供方言功能(dialect,可以转换为多种数据库语法),减少学习成本与迁移数据库的成本;
  • 面向对象,可读性强,开发效率高;
  • 防止 sql 注入攻击;
  • 搭配数据库迁移,更新数据库方便。

缺点

  • 需要语法转换,效率比原生 sql 低;
  • 复杂的查询往往语法比较复杂(可以使用原生 sql 代替)

15. OpenSpec 工作流

变更驱动的规约开发流程,按顺序推进:

proposal -> specs -> design -> tasks -> apply

  • proposal:提案,描述要做什么、为什么
  • specs:规约,定义行为细节
  • design:技术方案设计
  • tasks:拆任务清单
  • apply:按任务实施落地

16. LangChain 核心组件与 LCEL

16.1 核心组件

langchain 核心组件:Models(模型)、Prompt(提示)、Indexes(索引,现在更多叫 Retrieval 检索)、Memory(记忆)、Chains(链)、Agents(代理)。

16.2 LCEL

LCEL = LangChain Expression Language,LangChain 表达式语言。

文档:https://langchain-zh.cn/oss/python/langchain/overview

16.3 invoke 是什么

  • invoke 直译:调用、执行。
  • 在 LangChain 里:执行一个 Runnable 的同步主方法。
  • 只要是 Runnable(prompt、llm、parser、整条 chain、RunnableLambda、RunnableParallel),全都有 .invoke(),所以到处都能看到。

17. Pydantic

Python 最火的数据校验 & 解析库,核心:用类型提示定义数据长啥样,运行时自动校验、自动转类型,少写一堆 if 判断。

18. 向量数据库与术语(Weaviate / FAISS)

18.1 Weaviate 是什么

  • Weaviate 是 Go 语言开发的开源云原生向量数据库(AI Database)。
  • 它同时存储原始业务对象(文档 / 元数据) + 高维向量,可以把向量语义检索、关键词检索、结构化过滤放在同一个数据库完成,是做 RAG、语义搜索、多模态检索、AI Agent 记忆的主流组件。
  • 区别于 FAISS(只是检索库,无持久化、没有 HTTP 服务),Weaviate 是完整可直接上线的数据库服务。

18.2 FAISS

FAISS = Facebook AI Similarity Search

  • 直译:脸书人工智能相似度搜索库
  • Meta(原 Facebook)团队起名时,把项目全称缩写拼成 FAISS,刚好发音和 face 一致,方便口头交流,就直接当作一个单词来念,不是取自某个古老单词 / 拉丁语词根。

18.3 Weaviate 读音

Weaviate 音标:/ˈwiːviˌeɪt/

18.4 Weaviate 词源、原本含义

Weaviate 不是英语现成原生单词,是项目造出来的新词,词根来自动词 weave /wi:v/。

weave

词义:编织;交织

  • 名词:weaver 编织工;weaving 编织

开发者取 weave「编织」这个意象:

原始文档、元数据、向量、语义关系编织、交织在一起,构成一个数据库。

  • Weaviate = weave + 后缀变形,造出来的专有名词。
  • 读音:/ˈwi:viˌeɪt/ WEE-vee-ayt

拆解构词

weave → weavi- + -ate(动词后缀,表「使...化」)

字面意象:使之编织、进行编织

19. 术语:Hugging Face / endpoint

19.1 Hugging Face 公司 & AI 开源平台

  • 全球最大开源大模型仓库,放 LLM、扩散模型、数据集,Transformers 库就是他家做的。
  • 图标就是一个卡通机器人,两只手抱着自己的脸,所以叫 Hugging Face。

19.2 endpoint

  • 拆分:end(末端)+ point(点) → 端点
  • 通俗理解
    • Endpoint = 服务对外暴露的一个访问地址(URL)
    • 就是你程序可以发 HTTP 请求过去的那个接口地址。

19.3 nano 完整释义

一、英文词根 / 计量前缀(最通用)

读音:英 /ˈnænəʊ/ 美 /ˈnænoʊ/

核心定义:前缀 nano-,源自希腊语 nanos(侏儒、极小),国际单位制符号 n,代表 10⁻⁹(十亿分之一)

常用单位:

  1. nanometer (nm) 纳米:10⁻⁹ 米,微观尺度
  2. nanosecond (ns) 纳秒:10⁻⁹ 秒,芯片 / 高速通信用
  3. nanogram (ng) 纳克:微量质量单位

衍生词汇:nanotechnology 纳米技术、nanoparticle 纳米颗粒

二、Linux/macOS 终端 GNU Nano 编辑器(程序员最常指)

定位:Linux 命令行极简文本编辑器,对标 Windows 记事本,新手首选,替代复杂的 Vim/Emacs,绝大多数系统预装。

20. Celery 启动命令(imooc-llmops)

启动 worker 前先确认 Redis 在跑:

bash
redis-cli -a llmops123456 ping   # 返回 PONG 就好

启动 Celery worker:

bash
cd /Users/guowangyang/Downloads/imooc-llmops/api
celery -A app.http.app.celery worker -P solo -c 1 --loglevel INFO

用虚拟环境里的 python 启动:

bash
cd /Users/guowangyang/Downloads/imooc-llmops/api
.venv/bin/python -m celery -A app.http.app.celery worker -P solo -c 1 --loglevel INFO

参数说明:

  • -P solo:单线程执行池,macOS / Windows 调试常用,避免 prefork 子进程问题
  • -c 1:并发数 1
  • --loglevel INFO:日志级别

21. 大模型采样温度 Temperature

T 越低,高概率词(很好)更容易被选中,低概率词(很差)几乎不会出现。T=1 时,保持原始概率分布,既有一定随机性,又不会太离谱。T 越高,所有词的概率差距变小,低概率词也有较大机会被选中。

这就回应了为什么 T 越高,输出的文本越不重复,越有创造力,因为每个词的选中概率都比较接近。

21.1 Temperature 的三种典型值

Temperature 值效果适用场景示例
0-0.3完全确定性,总是选最高概率的词事实问答、代码生成、客服机器人「Python中如何读取文件?」
0.7-1.0平衡创造力与合理性日常对话、适度创意写作、头脑风暴「给我一些周末活动建议」
1.5-2.0高创造力,可能出人意料艺术创作、科幻故事、打破常规「写一首关于AI的诗」

21.2 延展题:T=0 时输出完全一致吗?

题干:当 Temperature 设置为 0 时,同样的 Prompt 调用多次,大模型的输出在文本层面完全一致。

正确答案:错误

解析:这是一个非常容易踩的坑。理论上 T=0 等价于贪心采样,每一步都选最高概率词,输出应当确定。但在工程实现上,主流 API(OpenAI、DeepSeek 等)在 T=0 时仍可能出现微小差异,原因来自:并行计算浮点误差、负载均衡时调度到不同 GPU、模型多副本部署、以及后端对 logits 的轻微扰动优化。因此要真正「完全可复现」,需要同时固定 seed、关闭流式输出、指定模型版本,而不仅仅是把 Temperature 设为 0。

21.3 Top-K 采样(K 值截断采样)

核心定义

模型预测下一个 token 时,先算出全部词汇表 Vocab 每个 token 的概率分布,只保留概率最高的前 K 个 token,剩下所有 token 概率直接置 0,再在筛选后的 K 个里面做随机采样。

完整执行流程

  1. 解码器输出 logits → 经过 Softmax 归一化得到 0~1 概率总和 = 1 的概率分布;
  2. 对所有 token 概率降序排序;
  3. 截取前 Top K 个最高概率 token,其余 token 概率清零;
  4. 对保留的 K 个概率重新归一化;
  5. 按新概率随机抽取一个作为当前输出 token;
  6. 循环迭代生成下一个 token,直到结束符。

参数影响

  • K=1(Greedy 贪心采样):永远选概率最大的 token,输出最稳定、重复度高、容易陷入循环;
  • K 增大:候选词变多,随机性、创造性变强,容易出现逻辑跑偏、胡说;
  • 常用区间:K=5/10/20/50

优缺点

  • ✅ 优点:计算简单、可控性直观
  • ❌ 缺点:固定数量截断,不考虑概率集中度。比如前 2 个 token 占 99% 概率,依然强制保留 K=20 个,引入不必要噪声

21.4 Top-P 采样(Nucleus Sampling 核采样)

核心定义

按概率累积阈值截断,而非固定个数。把 token 按概率从高到低累加,直到累积概率 ≥ P,只使用这一小簇(Nucleus 核心集合)token 采样。

执行流程

  1. 同样 Softmax 得到全量 token 概率;
  2. 降序排列 token;
  3. 依次累加概率,直到总和 ≥ 设定的 P 值(如 0.9);
  4. 只保留参与累加的这部分 token,其余清零;
  5. 归一化后随机采样输出 token。

直观例子

P=0.9:

  • tokenA:0.6,tokenB:0.3,tokenC:0.05,其他 0.05
  • 累加 0.6+0.3=0.9,刚好达标,只保留 A、B 两个候选,抛弃 C 和后面所有低概率 token。

参数影响

  • P=1.0:等价于全量随机采样,自由度最大,幻觉最多;
  • P 趋近 0:等价于贪心采样,极度保守;
  • 工业常用:P=0.7~0.95

21.5 Top-p vs Temperature:有什么不同?

两者都控制采样,但方式不同:

维度TemperatureTop-p
作用对象调整所有词的概率:低温强化高概率词、压制低概率词;高温拉平概率分布只选择头部的词:低 p 只考虑最可能的几个词,高 p 考虑更多词
动态性固定地调整分布,不管概率分布情况如何,调整方式都一样动态地选择候选集:模型很确定 → 候选集小;模型不确定 → 候选集大
取值示例——P=1.0 取所有词(等于没限制);P=0.9 只取累积概率到 90% 的词;P=0.5 只取累积概率到 50% 的词

21.6 采样流水线的完整顺序

plaintext
1. 模型输出原始 logits
2. Temperature 温度缩放(最先执行)
3. Top-K 截断过滤
4. Top-P(Nucleus)核采样截断过滤
5. 可选:重复惩罚、最小概率阈值等其他修正
6. Softmax 归一化概率分布
7. 随机采样选出下一个 token

22. 编译型 / 解释型 / JIT 关键概念区分

  1. 源码编译成机器码:C/C++、Go、Rust → 编译型
  2. 源码编译成字节码,虚拟机解释字节码:Python、Java
    • Java 有 JIT,会把字节码热点编译机器码
  3. JIT 即时编译:运行时把代码动态编译成本地机器码,JS (V8)、Java HotSpot 都有。

很多人混淆:「只要有编译步骤就是编译语言」。 重点看:编译产物是不是 CPU 可以直接执行的机器码。生成字节码≠编译型。

快速小结

  • JS:不是纯解释,V8 带 JIT 编译
  • Python:不是编译型,是字节码解释语言
  • Go:标准编译型语言,输出原生二进制

拓展对比

  • Java:源码→class 字节码,JVM 解释 + JIT 编译(混合)
  • C#:.NET CLR,IL 中间码,JIT/AOT 编译

23. Docker 端口映射 -p

格式:-p 宿主机端口:容器内部端口

text
-p A:B
A = 你电脑(宿主机)的端口
B = docker 容器里面程序监听的端口

拿 Weaviate 的例子:

  • 8080容器内部 Weaviate REST 服务固定端口,写死不能改,容器里程序就监听 8080。
  • 8081你本机电脑上对外暴露的端口,这个数字你可以随便改。

23.1 举两个例子

  1. 原版:-p 8080:8080

    本机 8080 → 映射到容器的 8080,访问 http://127.0.0.1:8080

  2. 如果本机 8080 已经被别的程序占用,就换本机端口,写成 -p 8081:8080

    本机 8081 → 映射容器内部 8080,访问 http://127.0.0.1:8081

注意:冒号右边永远是容器内部端口,不能乱改;冒号左边是你电脑的端口,可以自由换数字。

23.2 另外一个端口 -p 50051:50051

  • 容器内部 Weaviate gRPC 端口固定是 50051
  • 左边 50051 是本机端口;如果本机 50051 被占用,可以写成 -p 5050:50051
  • 此时本机访问 gRPC 就是 127.0.0.1:5050

23.3 总结记忆

-p 【本机端口】:【容器固定端口】

  • 冒号右边:容器内部程序写死的,不能随便改
  • 冒号左边:本机端口,冲突就换数字(1-65535 之间随便选个没被占用的)

24. DeepSeek V4 Pro 规格与 Kimi K3 对比

DeepSeek V4 Pro 完整规格:

  • 架构:MoE 混合专家模型
  • 总参数:1.6 万亿(1.6T)
  • 实际推理激活参数:仅 49B(490 亿)
  • 上下文窗口:100 万 tokens,最大输出 384K
  • 训练数据:33T tokens
  • 开源协议:Apache 2.0,权重公开可商用
  • 定位:长文本、代码 Agent

对比:Kimi K3 与 DeepSeek V4 Pro

对比维度Kimi K3DeepSeek V4-Pro
总参数量2.8T(更大)1.6T
激活参数约几十 B49B
行业地位全球第一开源模型全球第二大开源模型
优势多模态能力更强推理成本极低、代码 / 中文能力突出

25. 从 CloudAgent 沙箱把文件搬到本机

25.1 背景:为什么不能直接下载

tdocs-cloudagent 控制台的 /e2b/file 接口把响应当文本处理:

js
const text = await upstream.body.text();          // 按 UTF-8 解码
res.status(...).type("text").send(text);          // 按 text 发出

.text() 对 pptx / xlsx / docx(本质都是 zip 二进制)会做 UTF-8 解码,字节直接损坏, 下载下来打不开(终端里会看到 PK + [Content_Types].xml 之类的乱码)。

所以要么绕开它(base64 搬运),要么把它改对。

25.2 方法一:base64 搬运(不改代码,临时用)

① 沙箱里编码

bash
cd /workspace/ppt_one/output
base64 -w0 single-digit-1.pptx > /tmp/f.b64
# -w0 = 不换行,输出单行;不加会每 76 字符插一个 \n
wc -c /tmp/f.b64     # 25251 字节 → 约 33668 字符

② 复制前先确认没被截断

bash
tail -c 200 /tmp/f.b64

zip 类文件的 base64 结尾会带 UEsFBgAAAA...,这是 PK\x05\x06 (End of Central Directory)。能看到它就说明文件尾完整

③ 复制到本机

33KB 一般能一次贴完。建议先用 pbpaste 从剪贴板落盘,避免终端自动换行:

bash
# macOS:从剪贴板取,顺手去掉可能被掺入的换行/空格
pbpaste | tr -d '\n\r ' > /tmp/f.b64
wc -c /tmp/f.b64     # 应该 ≈ 33668

④ 本机解码

⚠️ macOS 的 base64 不接受位置参数,下面这种 Linux 写法会报 base64: invalid argument

bash
base64 -d /tmp/f.b64 > out.pptx      # ❌ macOS 不支持

正确写法(二选一):

bash
# 方式一:-i 指定输入
base64 -d -i /tmp/f.b64 > ~/Downloads/single-digit-1.pptx

# 方式二:stdin 重定向(Linux / macOS 通用)
base64 -d < /tmp/f.b64 > ~/Downloads/single-digit-1.pptx

macOS 的 base64 用法是 base64 [-Ddh] [-b num] [-i in_file] [-o out_file], 输入必须-i 或 stdin。

⑤ 验证(这步不能省)

bash
file ~/Downloads/single-digit-1.pptx
# 期望:Zip archive data / Microsoft PowerPoint 2007+

unzip -t ~/Downloads/single-digit-1.pptx
# 期望:No errors detected in compressed data

open ~/Downloads/single-digit-1.pptx

unzip -t 才是判据:

输出含义
No errors detected✅ 完整,能正常打开
missing bytes / zipfile is truncated❌ 复制被截断,要分段重搬

更稳的替代(python 容错更好,会忽略空白):

bash
python3 -c "
import base64, pathlib
src = '/tmp/f.b64'
dst = pathlib.Path.home() / 'Downloads/single-digit-1.pptx'
dst.write_bytes(base64.b64decode(pathlib.Path(src).read_bytes()))
print('written', dst, dst.stat().st_size, 'bytes')
"

输出的字节数应该和沙箱里 ls -la 看到的完全一致(例:25251)。

25.3 方法二:改 /e2b/file 支持二进制(一劳永逸,推荐)

tdocs-cloudagent/server.js/e2b/file 改成读原始字节:

js
app.get("/e2b/file", async (req, res) => {
  try {
    const sess = sessFromEnvdReq(req);
    const filePath = req.query?.path;
    if (!filePath) {
      res.status(400).json({ error: "缺少 path" });
      return;
    }
    const endpoint = assertEnvdTarget(sess.dataPlaneEndpoint, sess.acpToken);
    const fileURL = `${endpoint}/files?${new URLSearchParams({ path: String(filePath) })}`;
    const upstream = await request(fileURL, {
      method: "GET",
      headers: { "X-Access-Token": sess.acpToken },
      headersTimeout: 30000,
      bodyTimeout: 60000,
    });
    // 读原始字节:pptx/xlsx/docx 都是 zip 二进制,用 text() 会按 UTF-8 解码导致损坏
    const buf = Buffer.from(await upstream.body.arrayBuffer());
    const ct = String(upstream.headers["content-type"] || "");
    res.status(upstream.statusCode)
      .set("Access-Control-Allow-Origin", "*")
      .set("Content-Type", /^text\/|json/.test(ct) ? ct : "application/octet-stream")
      .set("Content-Disposition", `attachment; filename="${path.basename(String(filePath))}"`)
      .send(buf);
  } catch (err) {
    res.status(err.status || 502).json({ error: String(err.message || err) });
  }
});

path 模块 server.js 顶部已 import,不用额外加。)

重启后一条命令下载:

bash
curl -sS "http://localhost:3100/e2b/file?sid=<sid>&path=/workspace/ppt_one/output/single-digit-1.pptx" \
  -o ~/Downloads/single-digit-1.pptx

file ~/Downloads/single-digit-1.pptx
open ~/Downloads/single-digit-1.pptx

sid 从控制台 Connect 后的 /connect 响应里拿,或浏览器 Network 面板看 /e2b/* 请求的 X-Session-Id

25.4 只有大文件(>100KB)才需要分段

bash
# 沙箱
split -b 3000 /tmp/f.b64 /tmp/part_
ls /tmp/part_*

# 逐个 cat 复制,本机按顺序追加
pbpaste | tr -d '\n\r ' >> /tmp/f.b64

25.5 总结记忆

  • zip 类文件(pptx / xlsx / docx)走 /e2b/file 必坏 —— 因为 .text() 按 UTF-8 解码
  • macOS base64 必须 -i 或 stdin,不能直接跟文件名(和 Linux 不一样)
  • 判完整性用 unzip -t,别只看文件大小对不对
  • 看文件尾:base64 里的 UEsFBgAAAA = zip 的 EOCD 标记,有它说明没截断
  • 临时用走 base64,经常用就把 /e2b/file 改成二进制传输

26. redis-cli 登录与密码认证

bash
redis-cli
  1. 已进入交互式页面,输入 auth <密码>

  2. 还没进入cli,登录的时候带上密码 redis-cli -a <密码>

27. TKE(Tencent Kubernetes Engine)

TKE 是 Tencent Kubernetes Engine(腾讯云容器服务)

28. BCS(Blueking Container Service)

BCS 是 Blueking Container Service(蓝鲸容器服务)

29. ETC(Easy To Change)

ETC, Easy To Change

30. 知识的层级与重要性

知识有层级、有轻重之分,但不是知识自带高贵/低贱,而是相对于你的目标、场景、问题来划分。

30.1 四层结构(由底层到表层)

层级说明例子特点
底层原理(第一性)事物为什么这样运行,讲因果与机制物理定律、逻辑、概率统计、认知偏差、C++ 内存模型迁移最强、学得慢、长期有效;改底层会改变上层全部判断
框架/模型(中间层)原理封装成的思考框架水锤效应、交叉熵、RAII、CAP、SWOT、倒车入库流程拿来直接分析问题;依赖底层,环境变了部分失效
事实/案例/经验(表层)具体数据、地址、版本号、参数冷胎胎压 2.9、服务中心门牌号、string::npos 的值、某 API 参数学得最快、高度特定、易过期、记忆负担大
碎片见闻(信息)八卦、零散观点,无逻辑支撑偶然刷到的说法算不上成型知识

层级诊断:表层知识出问题,改表层常常无效,要往下挖框架、挖底层原理。 例:轮胎胎压老掉 → 表层是"打气" → 中间层是胎压变化规律 → 底层是气体热胀冷缩 + 密封泄漏机理。

30.2 重要性 = 对目标的贡献度(相对,没有绝对)

「重要知识」的三个特征:

  • 高频复用:工作生活中反复遇到,大量问题依赖它
  • 杠杆高:学会一条,能解释几十上百个现象、解决一大类问题
  • 稳定性高:十年八年不变,不容易过时

对做后端 + AI 工程来说:逻辑、计算机底层、统计学、系统思维 = 高重要;某个 API 的具体参数 = 低重要,查文档即可。

「不重要知识」不是没用,只是杠杆很低:只在极特殊场景用一次、需要时可当场查询、更新迭代快。

关键区分:重要知识要理解内化;不重要的知识不用死记,只记「去哪里查」。

30.3 三个常见误区

  • ❌ 底层知识一定比表层高级:日常开车不需要精通气体热力学,记住"冷胎 2.9"就够。脱离目标,底层原理也会变成无用负担
  • ❌ 冷门知识 = 没用的知识:在爱好场景下冷门知识就是重要知识(研究床垫时椰棕乳胶细节很重要)
  • ❌ 书本知识重要、生活经验低级:层级是结构划分不是价值歧视,很多实践经验是高价值的中间层知识

30.4 对应的学习策略

  • 高重要(底层/框架):深度理解、反复练习、内化进脑子
  • 中等重要(框架):理解逻辑、记住核心概念,细节可查阅
  • 低重要(事实类):不硬背,只记「在哪里查」,需要再检索

举例对照:

  • ✅ 重要:缓冲区是什么、水锤产生原理、::-> 语义、冷胎测胎压规则(反复遇到、能迁移)
  • ⚪ 次要:服务中心精确门牌号、string::npos 的具体数值(用时再查)

30.5 编程技能落在哪一层

编程三层都混合,但真正的硬实力在前两层

  • 底层原理:计算机组成、内存模型、CPU 执行、网络原理、复杂度、并发本质 —— 杠杆极高,几十年不变
  • 中间框架:RPC 模型、RAII、协程、Redis 设计模式、Transformer 架构、设计模式
  • 表层事实:某个函数 API、库版本、语法细节、命令参数 —— 易过时,可以现查

很多人只堆积第三层(记 API、记语法),版本一变全部清零。

30.6 引申:人的价值 ≠ 工具效用

  • 在任务/分工里:人有工具效用的相对差异,依附场景,可翻转(锤子拧螺丝不如螺丝刀,螺丝刀砸钉子不如锤子)。写底层的工程师放到育儿场景,效用未必高于擅长照料的家人
  • 作为人本身:生命尊严没有高低等级,不可排序、不可相对化。能力强收入高 ≠ 生命更重要;贡献小技能普通 ≠ 生命次要
  • 对自己:可以理性评估并提升自己的工具效用,但不要把它当成自我人格的评分
  • 对他人:合作中可客观评估能力与分工,但不用工作产出评判一个人的人格高低

混淆这两者,是自我否定和等级偏见的根源。

31. 知识的核心属性维度

维度一侧另一侧例子
时效性稳态(半衰期极长):逻辑、数学、组成原理、统计学、认知规律易衰变:框架版本号、库 API、平台接口、门店地址C++ 内存模型几十年稳定;第三方库函数两年可能废弃
可迁移性高迁移:概率思维、调试排错思维、系统拆解、因果分析领域绑定:某游戏 bug 的绕过方案、某业务独有字段、某公司内部流程写代码/修家电/分析胎压都能用的才算高迁移
形态显性:公式、语法、接口文档、操作步骤,看文字能学会隐性(默会):代码嗅觉、倒车手感、debug 直觉、听懂弦外之音高手的差距多在隐性知识,只能靠实践沉淀,看书拿不到
颗粒度粗:CAP、Transformer 整体架构、水锤原理细:某个函数入参、特殊异常、特殊配置粗颗粒概括一大类现象
逻辑因果:知道内在机制,可预测、可干预、可改造相关:只看到 A、B 一起出现,不知机理("下雨就卡"但不知路由受潮)相关性知识换环境容易翻车
杠杆高杠杆:一条解释/解决一大片低杠杆:只解决一个狭小问题杠杆相对目标才有意义
可压缩性可压缩:海量现象浓缩成少数原理公式不可压缩:离散事实只能逐条记忆学习的本质之一 = 把零散事实压缩成原理与模型
可证伪性可证伪:能设计实验证明它错(科学、工程)不可证伪:审美、个人偏好、部分价值判断"温度升高胎压上升"可验证
依赖性知识有依赖链:不懂内存难真懂指针,不懂概率论看不懂交叉熵——底层原理是地基
层级区分数据 → 信息 → 知识——2.7(数据)→ 胎压略低于标准(信息)→ 冷胎标准/热胎上涨/别热胎放气(知识)

记忆清单:层级 / 杠杆 / 时效性 / 可迁移性 / 显性·隐性 / 因果·相关·可证伪 / 颗粒度·可压缩性·依赖链 / 数据→信息→知识。

学习启发(结合编程)

  • 优先吸收:高迁移、高杠杆、长时效、可压缩的因果类知识
  • 零散细粒度事实不死记,知道去哪查
  • 显性知识看书和文档,隐性知识必须动手实践
  • 看清依赖链,基础缺失则上层只会浮于表面

32. 刷抖音能摄入知识吗

一句话结论:能摄入,但绝大多数是「表层、低杠杆、相关性为主的碎片信息」;少数优质内容能给到框架甚至底层原理。最大问题不是没有知识,而是质量不稳定、容易混入伪知识、且很难自动搭建知识体系

32.1 能拿到什么

  • 事实类信息(最多):胎压标准、水锤是什么、语法小技巧、生活窍门。细颗粒、单点、显性,拿来即用但迁移弱、易忘
  • 少量中间层框架:硬核博主会讲交叉熵、系统设计等,属中等杠杆;但短视频为易懂常简化、省略边界条件,学得不完整
  • 隐性知识几乎拿不到:debug 直觉、代码嗅觉、复杂问题拆解、倒车手感,只能靠实践,短视频只能展示别人的结果

32.2 四个典型缺陷

  • 因果被简化成相关性:只抛结论"这样做就成功",省略约束条件,换个场景就失效甚至踩坑
  • 时效参差、伪知识混杂:科普/工程/医疗类短视频常有错误,部分是博主个人片面经验,很多不可证伪
  • 没有知识依赖链:算法按兴趣单点投喂,不按学习顺序。可能刷到 Transformer 但数学基础缺失 —— 只记住名词,无法推理
  • 杠杆普遍偏低:平台偏好短、爽、即时反馈的单点知识;高杠杆底层原理理解成本高、流量差

32.3 两种模式,结果完全不同

模式做法收获
被动漫无目的刷算法持续投喂碎片零散事实 + 大量观点见闻,难成体系;副作用是大脑习惯短反馈,之后难静心读长文、做深度推理。定位偏向消遣
主动检索式刷想了解水锤 → 搜关键词 → 看 3-5 个不同博主交叉验证 → 整理笔记 → 挂靠已有框架有效知识摄入,可补充框架与案例。此时抖音只是信息素材源,搭体系必须靠自己

32.4 与书本/课程对比

  • 书籍/系统课程:按依赖链设计、优先讲因果、体系完整,适合学高杠杆底层知识;缺点是慢、门槛高
  • 短视频:单点素材多、案例丰富、通俗类比好,适合快速获取事实与案例;缺点是碎片化、简化严重、易有错误

不能指望短视频替你搭建知识网络 —— 串联、深度思考、实践,必须自己完成。

33. 信息输入 → 知识摄入 → 能力内化

三层递进,一层比一层深。很多人把第一层当成"学到了"。

33.1 信息输入(最表层)

定义:感官接收到符号、文字、画面,信息流过大脑 —— 只是"看见、听见",不一定理解,不改变思维。

  • 例子:刷到"冷胎胎压 2.9"、刷到什么是水锤、刷到 C++ 的 :: 是什么;看书扫过一行代码
  • 特征:只接收原始素材不加工;看完很快遗忘;可大量快速获取;不代表会用
  • 判断标准:能复述那句话,但换个场景不会处理 → "我听过这个说法"

33.2 知识摄入(把信息变成自己的知识)

定义:理解因果逻辑,挂靠到已有知识网络,明白它为什么成立、适用条件与边界。

  • 胎压:不只记 2.9,还懂热胀冷缩 → 热胎胎压升高 → 热胎不能放气、偏低要怀疑慢漏气,并与气体物理知识挂钩
  • ->:::理解对象、指针、类作用域的差异与使用场景
  • 水锤:不只记"咚的一声",明白根源是液体惯性,知道什么场景发生、怎么缓解

特征:懂因果、知边界;与旧知识建立连接(不是孤立碎片);能做简单推理;但不一定动手熟练,缺少体感。 判断标准:能解释原理,能分辨什么情况下该结论不成立 → "我理解这件事"。

抖音可以做到这一层,但前提是主动思考、交叉比对、梳理逻辑;被动划视频一般停留在第一层。

33.3 能力内化(变成本领,含隐性知识)

定义:知识经反复实践、犯错、修正,变成直觉、手感、行为模式;遇到问题时不用刻意回忆知识点,直接正确反应。显性知识 + 隐性默会知识合在一起才是能力

  • 倒车入库:知道步骤是摄入;反复练后看后视镜直接打方向、不用背步骤,就是内化
  • 排查 bug:懂内存模型是摄入;看到异常代码一眼嗅到风险、快速定位,就是内化
  • 胎压:看到数值立刻判断是温度波动还是真漏气,该打气还是查钉子,整套判断下意识完成

特征:必须实践试错,听看都不够;生成隐性知识(手感、嗅觉、直觉);可迁移到没见过的变体问题;短视频和看书很难直接给到。 判断标准:遇到新的同类问题不用翻笔记也能处理,还能处理边界异常 → "我会做这件事"。

33.4 三层对照

层级核心动作刷抖音能否得到关键标志
信息输入看、听,接收素材✅ 非常容易能复述原话,不懂边界
知识摄入理解因果,链接已有知识⚠️ 需主动思考,被动刷做不到懂原理,知道什么时候不适用
能力内化动手实践、试错沉淀隐性经验❌ 几乎不可能,必须实操遇到新问题也能解决,形成直觉

33.5 常见误区

  • 把信息输入当成学会了:刷完一堆短视频感觉收获满满,遇到真实问题无从下手
  • 把知识摄入等同于能力:看懂教程觉得懂了,一写代码、一实操就翻车 —— 看懂 ≠ 会做

33.6 完整学习链路

信息输入 →(思考整理)→ 知识摄入 →(大量实践试错)→ 能力内化

短视频擅长第一步,书籍课程负责第二步,第三步只能靠自己动手。

编程的完整例子:刷视频看到 string::npos 是什么(输入) → 理解它是查找失败标记、知道什么时候踩坑(摄入) → 自己写代码踩过查找字符串的坑,再遇边界 bug 能快速反应(内化)。

34. AI 时代的价值转移与训练的本质

34.1 价值从"如何实现"转移到"实现什么"与"如何组合"

过去,价值体现在"如何实现"——能多快写出一个排序算法、多熟练掌握一个框架的 API。现在这些细节 AI 都能完成,价值转移到两点:

  • "实现什么":由你的认知和想象力决定
  • "如何组合":由你的工程能力决定

工程能力的定义也随之变化:过去体现在代码的细节实现上,现在体现在技术的组合架构上。

34.2 训练的本质:一句话概括

以降低 Loss 为目的,通过反向传播和梯度下降手段,用数据驱动的方式迭代逼近最优解。

要素内容
目的最小化 Loss(预测值与正确答案的差距)
手段反向传播——计算每个参数的梯度(责任大小);梯度下降——沿梯度反方向调整参数,确保 Loss 下降
方法参数更新公式:参数新值 = 旧值 - 学习率 × 梯度;学习率控制每次调整的幅度
策略学习率动态衰减——初期大步快跑,后期精细调整;海量数据迭代——数千亿次微调,量变引发质变

34.3 泛化:参数存储的不是答案,而是知识

关键在于泛化能力——死记硬背只能处理见过的问题,理解规律能应对没见过的新情境。

知识是抽象的规律,答案只是规律的具体体现。模型学会了规律,就能应对无穷变化的问题——这才是真正的智能,也是 ChatGPT 这类大模型感觉"聪明"的原因。

34.4 分布式表示与不可解释性

没人知道每个参数的含义,关键在于知识的分布式表示(distributed representation):

  • "猫吃鱼"这个知识不是存储在某一个参数里,而是存储在可能数百万个参数的组合中,非单个参数独立编码
  • 就像全息图:每一小块都包含整幅图像的信息,但都很模糊;只有组合起来才能看清完整图像
  • 与人类大脑惊人相似

后果:不可解释性 → 安全性、可信度、调试困难三个问题。

34.5 Dense vs MoE 架构对比

架构机制优势劣势
Dense(稠密,传统)每次推理使用全部参数(GPT-3 1750 亿参数每次全部参与)架构成熟、训练稳定;所有参数协同工作整体性强;不需要专家路由,实现简单(GPT-4、Claude 都采用)推理必须用全部参数,计算量大;相同参数量下推理速度比 MoE 慢;内存占用高、部署成本高
MoE(Mixture of Experts 混合专家)包含多个"专家"子网络,每次推理只激活部分专家,不同问题激活不同专家参数量大能力强;推理只激活部分参数,速度快;计算成本和内存占用相对可控训练复杂度高,需精心调优专家路由;不同专家可能学到不均衡的知识;对某些问题可能比 Dense 差

34.6 三个术语

  • 分布式表示:知识分散存储在多个参数的组合中,非单个参数独立编码
  • 推理:用训练好的模型对新输入进行计算并输出结果
  • 指代消解(Coreference Resolution):确定代词指向哪个实体的语言理解能力

35. 自回归:一个词滚雪球般生成一段话

自回归(Autoregressive):每次基于已生成序列预测下一个词,逐步构建完整输出的生成方式——用自身历史序列预测下一步,"用自身历史序列预测下一步"。

35.1 函数视角:ChatGPT 是一个"接收输入、返回一个词"的函数

plaintext
第1次调用:ChatGPT("今天天气怎么样?")            → "今天"
第2次调用:ChatGPT("今天天气怎么样?今天")         → "上海"
第3次调用:ChatGPT("今天天气怎么样?今天上海")      → "的"
第4次调用:ChatGPT("今天天气怎么样?今天上海的")    → "天气"

关键细节:每次输入的不仅是已生成的答案,还包括最初的问题。只输入答案会失去上下文——模型不知道在问天气还是日期。函数的输出又被喂回函数作为输入的一部分,这就是"自回归"名字的来源。

35.2 为什么不一次性生成整个回答

  • 技术上更合适:一次性生成完整句子,需要同时预测几十上百个词,每个词有近几万种可能,组合空间是天文数字;一个词一个词生成,每次只需做一次预测
  • 更符合人类写作习惯:人写当前这句也是基于前面已写的内容

这种"滚雪球"式生成让模型能生成任意长度的文本,只要上下文窗口允许。

35.3 缺陷与缓解:误差累积

每一步的小偏差会影响下一步的上下文,进而影响下下一步的预测。四种缓解方法:

  • 更大的上下文窗口:窗口是模型的"短期记忆",窗口越大越能"看到"更长的历史,减少因"忘记"导致的偏离
  • 更好的采样策略:在质量和多样性间平衡,如 Top-p、Temperature
  • 加入外部记忆:引入外部知识库,如 RAG(检索增强生成)
  • 思维链 CoT(Chain of Thought):让模型先"思考"再回答,减少一路生成导致的偏离

36. 语言模型的本质:香农游戏与三代演进

36.1 为什么每次回答都不一样

因为模型引入了随机性:不是每次都选概率最高的词,而是按概率分布随机抽样。设计动机:

  • 更像人类:同一问题有不同的表达方式,表达更生动自然
  • 创造力与准确性的平衡:完全确定性太僵化,完全随机太混乱

36.2 香农的核心洞察与语言模型的本质

  • 香农的核心洞察:语言不是随机的,而是有规律的,这种规律可以用概率来描述
  • 根据香农游戏(Shannon Game),语言模型的本质 = 计算下一个词出现的条件概率 P(下一个词|上文)

从"今天天气真"五个字,经过 Embedding 查找 → 96 层 Transformer 变换 → Softmax 归一化,最终得到词表中所有词各自的概率。

36.3 规律是学出来的:演绎 vs 归纳

  • 传统 NLP 是演绎法:语言学家总结语法规则 → 程序员写成代码 → 计算机执行(从规则到应用)
  • 现代语言模型是归纳法:收集海量文本 → 模型自己从文本中学习规律 → 规律自动"浮现"在参数中(从数据到规律)
  • ChatGPT 的成功证明:在足够大的数据和足够多的参数下,归纳法可以超越演绎法

演绎例子:经典三段论(所有人都会死 → 苏格拉底是人 → 苏格拉底会死);数学证明几乎全是演绎法(三角形内角和 180°)。

💡 做题小坑:演绎得出的结论一定正确(前提对就对);归纳得出的规律只是统计猜想,可能被反例推翻(比如发现黑天鹅)。

36.4 Self-Attention 的优势

  • 无损传递:任何两个词之间直接连接,不经过中间层
  • 智能筛选:自动计算哪些词重要,把注意力放在关键位置
  • 并行计算:所有词同时处理,速度快
  • 理论上无限长度:不管句子多长,每个词都能"看到"所有其他词

36.5 语言模型三代演进

代次模型年代核心原理优点核心局限形象比喻
第一代N-gram(n 元语法)90-00 年代统计共现频次,P(w_{next} \mid 前\ n-1\ 个词),靠计数算条件概率简单、训练快;直接统计词语搭配只能看很近的少数前文,无法捕捉长距离依赖;没见过的组合概率为 0记忆只有 3 秒,只能看前面 2-3 个词
第二代RNN / LSTM2010 早期维护持续更新的隐藏状态,逐词串行传递记忆;LSTM 增加遗忘/输入/输出门能建模上下文,记住比 N-gram 更长的文本RNN 梯度消失(信息衰减);LSTM 改善但上限有限;串行计算慢传话游戏,信息不断丢失;LSTM 相当于智能笔记本,最多记住 100-200 词
第三代Transformer(Self-Attention)2017 至今自注意力,所有 token 互相直接访问,并行处理全部输入解决长距离依赖、自动聚焦关键词语、可并行训练快、支持超大上下文计算量随上下文长度增大而上升微信群聊,所有人直接看到全部消息;代词"他"可直接定位到远处的"张三"

关键对比记忆:N-gram 只看临近少数词(纯统计计数)→ RNN/LSTM 串行传话式记忆(信息逐步衰减)→ Transformer 全员直连的自注意力(长距离信息直接访问)。

N-gram 的关键假设 = 马尔可夫假设:下一个词的概率仅依赖前面最近的 n-1 个词,更早的上下文全部忽略(bigram 只看前 1 个词)。

37. 从 One-Hot 到 Embedding

37.1 One-Hot 编码及其两大问题

One-Hot = 恰好有一个位置是激活(热),其余全部关闭(冷)。

plaintext
类别:[猫, 狗, 鸟]
猫 → [1, 0, 0]
狗 → [0, 1, 0]
鸟 → [0, 0, 1]

两大问题:

  • 维度灾难:GPT-3 词表约 50000 个 token,每个词需要 50000 维向量,其中 49999 个是 0,极其浪费
  • 丢失语义信息:所有词之间相似度都是 0。"猫"和"狗"都是动物应该有相似性,One-Hot 把词变成了数字但丢了"意义"

Embedding:不用稀疏、高维、孤立的向量表示词,而用稠密、低维、充满语义的向量表示词。

37.2 "猫"是什么:指称问题

"猫"在不同系统中的表示:人类语言是汉字符号"猫";英语是字母组合 "cat";计算机是二进制编码;ChatGPT 是 12288 维向量 [0.23, -0.45, 0.67, ..., 0.12]

哪一个才是真正的"猫"?这是哲学上的指称问题(Problem of Reference)。语言学家索绪尔的答案:词的意义不在符号本身,而在于它与其他词的关系网络

  • 索绪尔:人文理论层,定义人类自然语言的符号本质、结构规则
  • Mikolov(Word2Vec):工程计算层,用神经网络把"语义关联、符号系统"数学化、向量化,让机器量化表征词语之间的结构关系

38. 预训练:自监督与 Next-Token Prediction

38.1 自监督学习:答案在原文里

原始文本"人工智能是计算机科学的一个分支",每个位置自动生成一道填空题:

plaintext
填空题1:输入"人工智能____"        答案:"是"        ← 不需要人工标注,原文里就有
填空题2:输入"人工智能是____"      答案:"计算机科学"

四大优势:

  • 答案在原文里:下一个 Token 就是标签,不需要人工判断
  • 样本自动生成:一句话能生成无数训练样本,把原文切开就行
  • 零标注成本:从互联网爬取文本即可
  • 规模爆炸式增长:监督学习可能只能标注 10 万条,预训练可以到 3000 亿 Token——这就是预训练能改变 AI 的根本原因

预训练阶段的核心训练任务 = 根据前文预测下一个 Token(Next-Token Prediction)

38.2 复习题:一句话能生成几个训练样本

"人工智能是计算机科学的一个分支"(6 个 Token)可以生成 5 个训练样本。解析:每个 Token 位置预测下一个 Token,共 5 个位置;T1 前面没有 Token,无法作为预测目标。3000 亿 Token ≈ 3000 亿次训练(每个序列最后一位不计,但相比总数可忽略)。

38.3 预训练的三个局限:为什么还需要微调

  • 只会"接龙",不会"回答"
  • 可能生成有害内容
  • 知识有时间限制

纯预训练模型(如 GPT-3)回答"太阳是什么时候开始绕着地球转的?"时,可能像文章续写一样继续接龙下去——说明预训练模型没有任务意识,不知道自己在被提问而不是在写文章。

38.4 监督微调(SFT)标注四大规范维度

  • 准确性:答案必须事实正确,不确定的必须标明"我不确定"
  • 有用性:直接解决问题,给出具体步骤或实例
  • 安全性:拒绝有害请求时不给具体实施方法
  • 格式规范性:清晰的段落和必要的项目符号排版

38.5 术语:petaflop 与 Gemini / Gemma

  • petaflop:flop = Floating Point Operation(浮点运算,算力基础单位);peta- = 10^15(千万亿)。1 PetaFLOPS = 每秒 10^15 次浮点运算
  • Google AI 双子星:Gemini(拉丁语"双胞胎",闭源旗舰主力模型,杰米尼);Gemma("宝石/嫩芽",开源轻量化下放模型,吉玛)

39. RLHF 与对齐(Alignment)完整拆解

RLHF = Reinforcement Learning from Human Feedback(基于人类反馈的强化学习)

39.1 「对齐」是什么

对齐(AI Alignment):让大模型的输出符合人类意图、价值观、偏好、道德规范、使用安全的过程。原始预训练模型只学统计规律、只会预测下一个字,存在答非所问、胡说八道、生成有害内容、不遵守指令等问题。RLHF 的最终目标就是把模型行为和人类标准"对齐"——听话、有用、安全。

两层对齐:

  • 意图对齐:听懂用户指令,给出有用、简洁、准确的回答
  • 价值对齐:拒绝有害提问,遵守伦理、法律、公序良俗

39.2 RLHF 三大标准步骤(OpenAI 原始版本)

  • Step 1:SFT 监督微调(Supervised Fine-Tuning):收集大量高质量人工标注问答对(Prompt + 人类优质回答),在预训练基座上做常规微调,产出 SFT 模型。作用是让模型先学会理解指令、模仿人类正确回答。缺点:人力成本极高,无法穷尽所有场景,没有统一打分标准
  • Step 2:训练奖励模型 RM(Reward Model):同一 Prompt 让 SFT 模型生成多个答案,标注人员排序打分(A 比 B 好、C 最差),用排序数据训练一个输出标量分数的奖励模型——分数越高越符合人类偏好。作用:把"人类主观喜好"量化成可计算数值
  • Step 3:PPO 强化学习优化:以 RM 奖励分数为目标函数,用 PPO(近端策略优化)迭代更新 SFT 模型;加入 KL 散度约束防止模型疯狂迎合奖励、偏离原始基座(模型退化)

一句话总结:人工标数据微调模型 → 人工打分训打分器 → 强化学习用打分器迭代模型

39.3 RLHF 的六大局限

  • RM 天然缺陷:奖励混淆(Reward Hacking,模型钻规则漏洞讨好打分器,如堆砌客套废话);人类标注主观不一致,RM 学到的是模糊平均偏好,容易中庸化;长尾场景奖励信号缺失
  • PPO 训练成本高、不稳定:算力巨大、周期长;超参敏感,KL 约束把控不好易模型坍缩(Mode Collapse)——输出同质化、创造力下降
  • "伪对齐"问题(最核心痛点):表象对齐而非本质对齐,模型只是拟合人类打分的表面特征(行为模仿,没有认知理解);经典例子是提示词越狱(Jailbreak),稍加诱导即可绕过安全约束
  • 价值观难以统一:标注人员的地域、文化、立场会植入模型,产生对齐偏见;不存在"绝对中立的对齐"
  • 依赖海量高质量人工标注:标注慢、贵、重复性高,规模化瓶颈;RLAIF(AI 反馈替代人工)只能缓解
  • 对基础能力提升有限:RLHF 本质是行为修正,不会提升知识、数学、逻辑、推理等底层基座能力——基座不行,再好的 RLHF 也只是"把笨模型教得听话一点"

39.4 行业改进方向

  • 算法迭代:DPO(直接偏好优化)、IPO、KTO 跳过独立奖励模型,直接用偏好数据做损失优化,更简单、算力更低、不易 reward hacking,已大规模替代 PPO 版 RLHF
  • 从"事后对齐"走向预训练阶段原生对齐:预训练时就灌入价值观、安全数据
  • 多层级对齐体系:微观(用户指令)→ 中观(社会规则)→ 宏观(公共伦理与长期安全)
  • 可解释对齐、红队对抗测试:让对齐过程可追溯、可审计,降低越狱风险
  • 多目标奖励优化:拆分有用性、真实性、安全性、简洁度多个奖励维度

背诵版:对齐 = 修正大模型行为使其匹配人类指令、安全规则与价值观;RLHF 三步 = SFT → RM → PPO;局限 = 奖励黑客、训练成本高、伪对齐易越狱、标注依赖重、不提升底层能力;新方向以 DPO 等偏好算法为主流。

40. Scaling Laws、涌现、ICL 与 CoT

40.1 Scaling Laws(缩放定律)

Scaling Laws 指的是:当你改变系统的规模时,性能如何变化的规律。2020 年 OpenAI 的论文震动了 AI 界——模型性能与规模之间存在可预测的数学关系

40.2 涌现(Emergence)及其理论假说

涌现能力的例子:算术推理(多步运算、百分比)、逻辑推理(三段论、反事实)、代码理解与生成(修复 bug)、语言翻译、常识推理(物理/社会常识)。

为什么会涌现,四个理论假说:

  • 隐式学习组合能力:模型学到很多"原子能力",规模够大时组合出新的"复合能力"
  • 跨越理解阈值:某些任务需要对概念有"足够深"的理解,达不到深度就完全做不了
  • 记忆容量达到临界点:某些任务需要记住很多模式和例子,容量不够就完全学不会
  • 涌现可能是幻觉:部分研究者认为涌现可能是"测量方式"造成的幻觉

40.3 In-Context Learning(ICL)

ICL(上下文学习):在 Prompt 中放入类比示例,让模型照着示例的模式完成任务。

40.4 思维链 CoT:定义、两种形式、为什么有效

CoT(Chain-of-Thought 思维链):在提示词中不仅提供问题和答案,还提供从问题到答案的推理步骤,引导模型逐步推理。

两种形式:

  • Few-Shot CoT(需要示例):在示例中展示推理步骤让模型模仿。结构:示例1: [问题]→[推理过程]→[答案] × N + 目标问题 → 模型自动生成推理 → 给出答案
  • Zero-Shot CoT(不需要示例):2022 年的惊人发现——只需在问题后加一句 "Let's think step by step"(让我们一步步思考),模型就会自动展示推理

为什么有效(深层原理):

  • 分解复杂问题
  • 利用自回归的优势:直接生成答案时模型只有一次机会;思维链下每生成一个推理步骤都成为后续推理的"输入",模型可以"边想边说",前面的推理帮助后面的推理
  • 自我纠错机会

41. Docker 镜像与容器常用命令 + 排错速查

41.1 镜像常用命令

bash
docker images                      # 查看所有镜像
docker pull 镜像名:tag              # 拉取镜像
docker rmi 镜像ID/镜像名:tag        # 删除镜像
docker rmi $(docker images -q)     # 删除全部镜像(谨慎!)
docker search xxx                  # 搜索镜像

41.2 容器日志命令

bash
docker logs 容器名/ID              # 查看容器日志
docker logs -f 容器名/ID           # 实时滚动看日志
docker logs --tail 100 容器名/ID   # 看最后 100 行

41.3 排错速查表

现象处理
容器名字已占用docker ps -a 找到残留容器,docker rm 名字
端口被占用 Bind failed-p 本机端口:容器端口,左边数字换别的
容器启动后立刻退出 / Restartingdocker logs 容器ID 看日志找原因
镜像拉取超时配置 docker 镜像加速器
run 报错但 ps 看不到容器容器启动失败,用 docker ps -a 看 Exited 状态

42. Weaviate 升级:hfresh 索引类型与 deprecated API

术语:retriever,美 /rɪˈtriːvər/,检索器(Weaviate 这类向量数据库的核心角色)。

42.1 问题根因

weaviate-client==4.18.3VectorIndexType 枚举只定义了三类:hnswflatdynamic。但 Weaviate Cloud 上的 Dataset collection 用的是 hfresh 索引类型(新的 HNSW+Flat 混合索引,结合 HNSW 搜索速度和 Flat 精确度)。旧版客户端不认识这个类型,VectorIndexType('hfresh') 构造时报 ValueError

42.2 修复内容

  • 升级 weaviate-client4.18.34.22.0hfresh 已加入枚举)
  • 修复 deprecated API:connect_to_wcs()connect_to_weaviate_cloud()(参数签名完全一致,只是方法名变了)

42.3 其他文件提醒

同目录还有 15 个文件也用了已废弃的 connect_to_wcs(不至于报错,只有 DeprecationWarning)。后续运行那些示例时建议批量替换为 connect_to_weaviate_cloud

43. 云业务的三个特性

往下讲之前先打个预防针:我们能拿到这些结果,因为我们是一个比较典型的云业务,有三个特征:

  1. 不是超级 App,由很多子产品组成——比如云原生下面就有 TKE、TKE Serverless 一大串——每个子产品就是几十人的小团队,适合从点上创新;
  2. 底层通常基于开源,数据库、Kubernetes、Hypervisor、存储、网络的 know-how 在模型训练时基本内化过,对我们这类业务天然更熟;
  3. 技术就是产品,虽然也要解决商业化,但只要聚焦技术突破,产品竞争力就能提升。

这三个特征让 AI 提效在我们这里效果特别显著,别的业务未必能直接复制。

44. 新的技术必须匹配新的生产模式

一个基础判断:新的技术必须匹配新的生产模式

45. AI 打破的三个旧假设

过去整套研发体系建立在三个假设上:

  1. 人的能力有边界,所以必须分工协同——AI 让一个人的能力变强了,分工的必要性还继续存在吗?
  2. 执行和试错成本高,所以必须先规划——但认知是在实践中积累的,规划又必须在实践之前做,等于在认知最缺乏的时候做最重要的决策,本身是个矛盾。AI 极大降低了试错成本,也许可以先做出来再修正。
  3. 人是独特的,经验只能靠培养和传帮带——AI 可以把经验复制,一个调通的 Agent 可以复制一万份、一亿份,组织可以大量并行。

这就产生了我们团队最核心的一条认知:

AI Agent 不是工具,是新的生产主体;AI 提效不是代码补全,是生产模式的重塑。

46. 超级个体,消除协同损耗

人的协同是必要的,但绝大多数低效都来自协同。做一个产品从想法到发布,要拉前端、后台、运维、产品、策划;两三个人开会信息就开始有偏差,人一多是指数级上升;每个人只对自己的岗位职责负责,不对最终结果负责,信息衰减、循环等待、责任稀释一层层叠加。

47. sheetagent:GAS shim 与 run_command 调用链

GAS = Google Apps Script,Google 表格的脚本 API。它的标志性写法是:

ts
const sh = SpreadsheetApp.getActiveSheet();
sh.getRange("A1:B10").setValues([[...]]);
sh.addConditionalFormat(...);

shim = 适配层/垫片:把 GAS 风格调用翻译成底层 MCP op

路径作用
TS 侧(主)mcp/src/runtime/shim/sheet.ts range.ts spreadsheet.ts chart.ts helpers.ts block_cache.ts提供 GAS 对象模型,把方法调用转成 op
运行时mcp/src/runtime/host.ts worker.ts bridge.ts protocol.ts invoke.tsWorker 线程跑脚本 → SharedArrayBuffer/Atomics 同步 RPC 把 op 传给主线程 → handleOp() 翻译成 SheetApi 调用 → 引擎
Python 侧…/scripts/shim/caps_cf.py export_fix.py run_cases.py …文件层 OOXML 兜底(引擎没 op 的那些)

关键点:shim 不是想调什么就能调什么——它受 protocol.ts 里的 OpName 联合类型约束。我把它抓出来了,这才是 run_command 的真实能力天花板。

47.1 完整调用链(拿 setValue 举例)

模型发给 MCP 的是一段脚本字符串

mcp__sheetagent__run_command
{ script: "const sh = SpreadsheetApp.getActiveSheet();
           sh.getRange('A1').setValue(42);" }

然后发生这些事:

模型(子代理)
   │  发起 MCP 调用,payload 是上面的脚本字符串

run_command.ts          ← 工具入口,收到脚本,调 runScript()

host.ts(主线程)        ← 起一个 Worker 线程,把脚本喂给它

╔═════════════ Worker 线程 ═════════════╗      ╔═════════ 主线程 ═════════╗
║ worker.ts:在沙箱里执行脚本              ║      ║ bridge.ts:handleOp()    ║
║                                       ║      ║                          ║
║  SpreadsheetApp ← spreadsheet.ts      ║      ║  收到 op:                 ║
║    (shim 伪造的 GAS 全局对象)          ║      ║   "range.setValue"       ║
║  .getActiveSheet() → SheetShim        ║      ║  按 OpName 分发翻译成:    ║
║  .getRange('A1')  → RangeShim         ║      ║   api.sheetSetCellValue()║
║  .setValue(42)                        ║      ║   ↓                      ║
║    ↑ 这一步不发网络请求!                ║ op   ║   SheetApi               ║
║      而是打包成一条 op 消息:            ║──────►   ↓                      ║
║      { name:"range.setValue",         ║ 共享  ║   upstream               ║
║        args:{row:0,col:0,value:42} }  ║ 内存  ║   ↓                      ║
║      然后 Atomics.wait() 睡觉          ║◄─────║   POST 127.0.0.1:39099   ║
║                                       ║ 结果  ║   (引擎真正干活)         ║
║  ……被叫醒,拿到返回值,继续执行下一行       ║      ║  把结果写回共享内存,叫醒   ║
╚═══════════════════════════════════════╝      ╚══════════════════════════╝
   │  脚本跑完 → ScriptResult { value, logs, opsCount }

run_command.ts → 包成 MCP 响应 → 回给模型

47.1.1 每一层回答一个“为什么”

① shim 是什么?——一个“假的 Google 表格 API”

模型写的是 SpreadsheetApp.getActiveSheet(),但你的进程里根本没有 Google 表格。shim/ 目录里的 spreadsheet.ts / sheet.ts / range.ts 就是伪装成 GAS 的空壳对象:每个方法的“里子”不是真执行,而是把调用打包成一条 op 消息("range.setValue" + 参数)递出去。

类比:你去银行柜台(shim)填单子,柜台不办业务,只把单子递进后台(主线程)。它叫 shim(垫片)就是因为夹在中间:上面顶着 GAS 的“面子”,下面接着自家引擎的“里子”

② 为什么要 Worker 线程 + SharedArrayBuffer + Atomics?——同步和异步打架

这是整条链最绕的地方,但原因很简单:

  • 模型写的脚本是同步写法setValue(42) 写完直接下一行,没有 await
  • 但真正落到引擎是一次异步网络请求(POST 39099)

同步的外壳包不住异步的内脏,怎么办?用两个线程玩“睡觉-叫醒”

Worker(跑脚本的)                     主线程(管网络的)
   发 op → Atomics.wait 睡觉 ──────►  收到 op,发异步请求
   (脚本暂停在这一行)                  请求回来,结果写进共享内存
   被 Atomics 叫醒 ◄────────────────  Atomics.wake
   拿到返回值,继续下一行

SharedArrayBuffer = 两个线程都能摸到的共享桌子Atomics.wait/wake = 桌子上的叫醒铃。这就是 protocol.ts(op 消息格式 + OpName 白名单)和 bridge.ts(主线程按 OpName 分发)存在的原因。

③ block_cache.ts 是干嘛的?——省网络

脚本里经常反复读同一个区域(先读后判断再写)。block_cache 把读过的数据块缓存住,第二次读不发请求。没有它,一个脚本可能扇出几百个重复读请求,把引擎打挂(历史上 12 万行扇出 237 路打挂过 sheetengine,才加的并发闸门)。

④ Python 侧那条为什么是独立的路?

注意它不在上面这条链里。scripts/shim/ 那些 Python 文件不走 MCP、不走引擎——它们是直接解压 xlsx 文件改 XMLxl/worksheets/sheet1.xml 里的 <colorScale><dataValidation> 节点)再打包回去。因为引擎压根没提供那些 op(工作簿设置、评论、原生色阶……),TS 这条链的天花板就是引擎的 OpName,够不着文件层,所以只能出文件层的工具兜底。

47.1.2 一句话版本

run_command = 让模型写 Google 表格风格的脚本;shim = 假装是 Google 表格、实际把每个方法翻译成 op 的空壳对象;Worker + Atomics = 用“睡觉-叫醒”把异步网络请求伪装成同步调用,骗过脚本的同步写法;bridge = 主线程收 op、按 OpName 分发成真正的引擎请求;Python shim = 引擎根本没有的能力,绕过引擎直接改 xlsx 文件。

验证一下你是否真的理解了,可以自测这个问题:为什么 interface.ts 里有 sheetUnsetFreeze,但 run_command 里取消冻结做不到? 答案:因为脚本里 sheet.setFrozenRows(0) 这种写法最终只能发 OpName 里存在的 op,而 OpName 联合类型里只有 sheet.setFreeze 没有 sheet.unsetFreeze——shim 的“面子”再全,protocol.ts 的 op 白名单不放行就出不去。

47.2 worker线程的op为什么会给主线程执行,直接在worker线程里执行不行吗,worker线程的作用是啥

这个问题问到架构根子上了。答案是:不行,会死锁。我从代码里给你逐层拆。

47.2.1 最本质的原因:sendSync 会把 worker 线程冻死

worker.ts 里 shim 发出一个 op 用的方法名:

js
channel.sendSync({ op: "ctrl.log", args: { level, msg } });
                    // ↑ 注意是 Sync

sendSync 内部是 Atomics.wait()——它会把整个 worker 线程完全阻塞,停在那一行。

现在假设 op 就在 worker 里执行:

worker 线程
  sh.getRange("A1").setValue(42)
    → 发 HTTP 请求(异步)           ← 需要事件循环来回调
    → Atomics.wait 等结果            ← 线程被冻死,事件循环停转
    → 请求的回调永远没人执行           ← 死锁 💀

异步请求的完成依赖事件循环把回调排进去;而 Atomics.wait 恰恰把事件循环卡死了。一个被冻结的线程,自己完成不了自己的异步 I/O。所以必须有一个"不被冻结"的线程替它跑网络——这就是主线程。

worker(可冻结,跑脚本)        主线程(不冻结,跑 I/O)
   发 op → 睡觉 ────────────►  收 op → 发请求 → 等回调
   (事件循环停了)               (事件循环正常转)
   拿到结果醒来 ◄────────────   结果写回共享内存 + 叫醒

bridge.ts 第一行注释就写明了分工:

主线程 bridge:接收 worker 发来的 op,翻译成对 SheetApi 的调用。

47.2.2 另外三个现实原因

api 对象根本传不进 worker

host.ts 创建 Worker 时传了什么:

js
new Worker(workerPath, { workerData: { sab, script, fileId, sheetId } })

只有共享内存、脚本、两个 id——没有 api。因为 Node 跨线程传数据走结构化克隆,克隆不了 HTTP 连接、socket、函数,更别说 UpstreamClient 里那个 MCP 长连接(StreamableHTTP 会话)。真传就得每个脚本重建一次 MCP 握手,代价太大。所以 api 只能留在主线程,worker 只能"委托"。

② 并发闸门必须集中

bridge.ts 里这个常量和它的注释,是我见过最实在的架构注释:

js
const READ_CONCURRENCY = 4;
// 线上出现过一次 12 万行 getValues 扇出 237 路、把 sheetengine 打到 502/503 的事故。
// 不设限就是拿自己的读请求打自己的服务端。

如果每个 worker 自己发请求,这个闸门就形同虚设(N 个脚本并发 = 无上限扇出)。集中到主线程,才能统一限流、去重、批处理。

③ 沙箱安全边界

worker.ts 第一行 import vm from "node:vm"——脚本跑在 vm 沙箱里,能看到的全局对象只有 buildShimGlobals() 注入的那些(SpreadsheetApp / Sheet / Range…),碰不到 fsnetprocess。而它唯一能对外产生影响的出口,就是 protocol.ts 里的 OpName 白名单

这是个很干净的安全模型:脚本再怎么折腾,也只能发白名单里的 op,出不去这个圈。

47.2.3 那 worker 线程的作用到底是什么?

四个,缺一不可:

  1. 提供一个"可以被安全冻死"的线程 —— 它阻塞时,主线程照样跑事件循环、处理网络、响应 MCP 心跳。这是最核心的作用。
  2. 沙箱隔离 —— vm + 只注入 shim,脚本碰不到宿主资源
  3. 崩溃/死循环隔离 —— 脚本死循环可以 terminate() 掉;host.ts 里专门处理 worker.once("exit") 提前退出的情况,主进程不受牵连
  4. 让"同步 API"成为可能 —— 这是它存在的根本原因(见下)

47.2.4 既然这么绕,为什么不干脆让脚本写成 async?

你可能会问:让模型写 await sh.getRange("A1").setValue(42) 不就完事了,何苦双线程?

worker.ts 其实支持顶层 await(它用的是 AsyncFunction)。但 shim 的 API 刻意做成同步的,因为:

  • GAS 就是同步的SpreadsheetApp.getRange().setValue() 在 Google 表格里从来不返回 Promise。模型训练语料里见过几百万次这种写法,改成 await 版它就陌生了,出错率上升。
  • 一旦要 await,脚本里每个调用都得 await,模型漏写一个就静默失败。

所以这是主动选择的取舍:为了保住"模型最熟悉的 GAS 同步语义",架构上付出"双线程 + 共享内存 + 同步 RPC"的复杂度。

一句话:worker 是"可以被冻住的演员",主线程是"永远不会停的后台"。演员念到需要取东西的台词就定住,后台把东西递上来,演员继续念——这样剧本(脚本)才能写成完全没有 await 的同步台词。

47.3 interface.ts 给谁用的,protocol.ts 给谁用的

这两个文件是同一条链上两个不同的闸门,服务完全不同的对象。

47.3.1 各自给谁用

1. api/interface.ts —— 能力契约(MCP 工具层用)

它定义 SheetApi 接口(69 个方法 + 一堆类型)。消费者:

怎么用
tools/*.ts(31 个工具)注入的 ctx.api.xxx(),只认接口不认实现
api/local_api/client.tsimplements SheetApi → 转发到本地 editor 39099
api/remote_api/client.tsimplements SheetApi → 转发到远端 sheet-mcp
runtime/bridge.tshandleOp 里拿 ctx.api 调真实实现
server.tsSHEET_API_MODE / PLATFORM 决定注入哪一套实现

一句话:它是「工具层 ↔ 后端实现」之间的编译期契约。工具想调什么,先得在这个接口里声明过。

2. runtime/protocol.ts —— 通信协议(run_command 通道用)

它定义 OpName 联合类型 + OpRequest / OpResponse 消息格式。消费者:

怎么用
runtime/invoke.tsmakeInvoke() 构造 op,参数受 OpName 类型约束
runtime/channel.ts跨线程搬运 OpRequest / OpResponse
runtime/bridge.ts主线程按 OpName 分发
shim/sheet.ts shim/range.ts shim/block_cache.ts发 op 时受类型检查

一句话:它是「worker 线程 ↔ 主线程」之间的运行时协议。脚本想发什么 op,先得在 OpName 白名单里。

47.3.2 把它们摆在一条链上

                    ┌─────────────── 路径 A:MCP 工具 ───────────────┐
模型 → 结构化参数 ──►│  tools/*.ts ──► interface.ts 契约 ──► AdapterApi ──► 引擎
                    └────────────────────────────────────────────────┘
                                       ▲ 闸门1:接口声明了没?

                    ┌─────────────── 路径 B:run_command ────────────┐
模型 → GAS 脚本 ───►│  shim ──► protocol.ts 的 OpName ──► bridge ────┼──► 同样的 AdapterApi ──► 引擎
                    └────────────────────────────────────────────────┘
                                       ▲ 闸门2:op 白名单放行没?

两条路最后汇合到同一个 AdapterApi 和同一个引擎。区别只在前面的闸门不一样。

47.3.3 回到 unsetFreeze 那个例子

闸门 1:interface.ts闸门 2:OpName结果
MCP 工具路径(range_ops.tssheetUnsetFreeze 已声明不经过能取消冻结
run_command 路径✅ 也能走到❌ 没有 sheet.unsetFreeze出不去

所以:

interface.ts = 能力全集(引擎能干什么)protocol.ts = 出口宽度(脚本能发出什么)

能力全集 ≠ 通道可达集。接口里有,不代表 run_command 摸得到。

47.3.4 一个很有说服力的佐证

你跑 npm run typecheck 时那 10 个 pre-existing 报错,正是闸门 2 在生效:

src/runtime/shim/sheet.ts(456,20): error TS2345:
  Argument of type '"sheet.setDimensionVisible"' is not assignable to parameter of type 'OpName'.

shim 想发一个 op,但 OpName 联合类型里没有它 → 编译期就报错。说明这个白名单是真闸门,不是摆设,而且缺漏是编译期就能发现的(比运行时炸好得多)。

47.3.5 比喻总结

  • interface.ts = 公司的《服务目录》,写明了公司能提供哪些服务
  • protocol.ts = 前台《受理窗口清单》,客户只能通过窗口办事
  • MCP 工具 = 内部员工,按目录直接调服务,不受窗口限制
  • run_command = 外部客户,只能走窗口,窗口没列的服务就办不了

所以你之前那个判断是对的:shim 的"面子"(GAS 方法)做得再全,protocol.ts 的 OpName 不放行,脚本就出不去 worker 这道门。 而这次 MR 新增的 range_ops 工具,是走"内部员工通道",不受这个窗口限制——这也是为什么它对 run_command 有增量价值,而不只是重复造轮子。

48. run_command 和其他工具的本质区别是什么,用哪个更优

这是个很关键的架构问题。我先把 shim 的能力面查清楚了——结论可能和你预期不同:run_command 现在几乎什么都能做

48.1 run_command 的真实能力边界 = shim 暴露了什么

runtime/shim/SheetRange 两个类的方法全集(脚本里能直接调的):

Sheet: addConditionalFormat / updateConditionalFormat / removeConditionalFormat / getConditionalFormats
       createFilter / setDataValidation / setDataValidationByColumns
       setFrozenRows / setFrozenColumns / insertImage / removeImage
       getPivotTable / getPivotTableDetail / addProtectRange / insertChart ...
Range: sort / createPivotTable / setFilters / merge / setBackground / setValues ...

也就是说你这次 MR 新增的能力里,排序、数据验证、冻结、筛选、条件格式、图片、透视表——run_command 全都能做Range.sort()sheet.setDataValidation()sheet.setFrozenRows() 都在)。

所以真相是:新工具几乎都不是"唯一路径",而是把「模型手写脚本」换成「填参数」

48.2 本质区别:脚本执行器 vs 结构化契约

run_command(脚本)专用工具(Tier-1)
本质通用脚本执行器,图灵完备固定语义的结构化契约
谁实现逻辑模型(现场写 JS/GAS)TS 代码(已写死 + 单测覆盖)
入参校验无,脚本字符串zod schema,类型/枚举强校验
前置成本HARD RULE:必须先 Read api-reference(几千 token)description 自带,零前置
出错面语法 + 0/1-based + 枚举值 + 双等号 bug只有参数填错
可观测脚本内 console,结构化弱tool.call / tool.result 结构化日志(带 ms、is_error)
能力 gating不能(永远开启)supports() 按 API 模式跳过注定失败的
内置兜底无,模型自己写 try/catch有(applyFilterRange 规避引擎报错、missing_sheet_id fail-fast、截断边界守卫)
批量/循环:一次脚本跑 100 步、读数据后再决定写什么弱:一次调用一个语义动作
网络往返1 次调用 N 个 opN 次调用 N 个 op(远端模式每个 100ms+)
白名单✅ 已在内❌ 要显式加

一句话概括:run_command 赢在灵活和批量,工具赢在确定性和可观测

48.3 哪个更优 —— 分场景,不是二选一

用工具更优:

  • 有固定预设、语义稳定的动作:色阶(N 段 BETWEEN 插值要手写会疯)、条件格式 17 个预设(布尔公式要写对)、冻结、数据验证、单条筛选
  • 要求结果稳定可复现的:审计验收(结构化日志可回放,静态染色那种假阳性工具也拦得住)
  • 模型容易写错的地方(0/1-based、枚举值、枚举名)

用 run_command 更优:

  • 需要读数据后再决定写什么(循环遍历、条件分支)——比如"找出所有含 A 的单元格并标记",工具做不了判断
  • 批量编排:一次脚本 100 步,远端/在线模式下省掉 100 次 HTTP 往返(本地 ms 级差异小,线上差 10 秒级)
  • 复杂多步逻辑:建子表 → 写数据 → 加透视表 → 调样式

风险提醒:run_command 批量扇出有坑——引擎有读并发闸门 READ_CONCURRENCY=4,大表扇出过多会把 sheetengine 打挂(历史上 12 万行扇出 237 路打挂过)。所以"批量"要在脚本里控并发,不是无脑循环。

48.4 对你这个 MR 的直接含义

  1. 收益定性要修正:这批工具的收益主要是成功率和稳定性(把"模型写对公式"变成"填参数"),不是"能力从无到有"。只有色阶/数据条/图标集/命名视图/钻取这几项是真正新增的表达能力(靠 TS 层拼),其余是路径优化。

  2. 当前收益 = 0:白名单没同步 → 子代理只能 run_command → 新工具一次都调不到。

  3. 同步白名单后,必须给 prompt 加路由规则,否则模型会继续无脑 run_command(它有路径依赖)。建议写法:

    场景走谁
    单条条件格式 / 色阶 / 数据条 / 冻结 / 数据验证 / 单条筛选专用工具
    需要遍历判断、批量 N 条、多步编排、先读后写run_command
    工具报错且明确是"引擎不支持"不重试,直接判未实现
  4. 顺带要改的sheet-agent-prompt.md.tmpl:484 那句「pivot tables / sort / filter 只能用 run_command」现在是错的,工具已经能做,留着会让模型永远绕开新工具。

a

你这个判断方向对,但要分一刀——关键看 OOXML 里有没有"原生表达"。我把引擎源码也翻出来了,先看事实。

一、引擎源码就在本仓库,加 op 很轻量

library/collab/editor/server/mcp/sheet_mcp_tools.cc   ← 工具路由(1304 行)
library/collab/editor/server/mcp/sheet/*.cc           ← 各能力实现(67 个 .cc)

加一个 op 的模式就这么简单(sheet_mcp_tools.cc 实测片段):

cpp
  // ---- set_freeze ----
  if (tool_name == "sheet_set_freeze") {
    auto err = editor.SetFreeze(args.at("sheet_id").get<std::string>(),
                                args.at("row_count").get<int64_t>(),
                                args.at("col_count").get<int64_t>());
    if (err && err->NotOK()) return McpError(err->ToString());
    return McpOk("Freeze set");
  }

所以"在引擎实现 op"不是空话,是本仓库内能做的事

二、但要分两类,你的直觉只对了一半

OOXML 有原生节点?推引擎的价值条数
色阶 / 数据条 / 图标集<colorScale> <dataBar> <iconSet>——引擎放了词表就是真原生对象,比现在 N 段 BETWEEN 模拟强一个量级30
工作簿设置 / 评论 / 名称管理器 / 浮动图片✅ 都有对应 XML 节点21
命名视图(filter view)OOXML 没有这个概念——引擎自己也得造一套存储,和 MCP 层用 __sheet_meta 隐藏子表本质一样4

所以 filter 视图恰恰不该推引擎:OOXML 每个子表只支持一个 autoFilter,压根没有"命名视图"这回事。引擎实现了也是内部自定义持久化,不会比你现在的隐藏子表方案更"原生"——反而你那张 __sheet_meta 用户还能看见、能排查。建议保留,不回退。

三、当务之急确实是引擎侧,但聚焦 A 类那 51 条

按 ROI 排(这些都是 OOXML 有原生表达的,推引擎 = 真根治):

优先级能力条数现状推引擎后
P0开放 CF 词表(COLOR_SCALE / DATA_BAR / ICON_SET)30TS 层模拟(分段/辅助列)真原生对象,color_scale.ts + formula_viz.ts 可整体删掉
P1浮动图片 op(drawing 层)5Python shim 改 OOXML原生
P2工作簿设置9Python shim原生
P3评论批注4Python shim原生
P4名称管理器3Python shim原生

P0 的收益最大:一旦引擎放开 CF 词表,你这次辛苦写的 buildScaleRules(N 段插值)、dataBarFormula(REPT)、iconSetFormula(PERCENTILE)全部可以删掉,换成一次原生 CF_COLOR_SCALE 调用,代码量和出错面同时下降一个数量级。

四、"改一下看看效果"——本地跑不通,但有个替代

坏消息:本地编译门槛高。library/collab/editor 是 CMake 项目,要 absl 等依赖,仓库里没有任何 build 产物(没编译过),而当前跑的 editor_sdk 是 npm 包里 228MB 的预编译二进制。首次全量编译以小时计,且要替换那个二进制才能验证 —— 本地"看效果"不现实,得走 CI 或引擎团队构建

好消息:有个几分钟就能验证的替代方案——直接用 Python 写原生 OOXML,验证"引擎开放后到底能做到什么程度"

往 xlsx 的 sheet1.xml 里直接写 <conditionalFormatting><cfRule type="colorScale">…</colorScale>
→ openpyxl 回读确认是原生 ColorScaleRule 对象(不是 N 段 cfRule)

这个验证有两个价值:① 证明技术可行(给引擎团队的需求单有实锤);② 跑通了可以先把 caps_cf.py 从公式方案升级成原生方案,30 条 case 立刻从"模拟"变"原生通过",不用等引擎。


要我现在做哪个?

  1. A(推荐,快):用 Python 写原生 <colorScale>/<dataBar> OOXML + openpyxl 回读验证,出一份实测报告 → 作为给引擎团队的需求单附件,同时可升级 caps_cf.py
  2. B:在引擎源码里加一个 op 原型(比如先加 sheet_color_scale),产出 diff 给引擎团队评审(本地不编译)
  3. C:先不动,我把 51 条 A 类需求整理成一份带 OOXML 节点规范 + 现有代码位置的需求清单

我倾向 A——它是唯一能"立刻看到效果"的路径,而且产出直接服务于推引擎。