2026-09 笔记
1. 存储单位与大模型参数量单位
1.1 存储单位(内存、显存、磁盘,字节 Byte)
描述文件、模型权重、数据集占用空间。
二进制(计算机底层):1024 进位;硬盘厂商十进制:1000 进位。
| 符号 | 全称 | 二进制值(内存 / 显存) | 十进制(硬盘厂商标注) |
|---|---|---|---|
| B | Byte 字节 | 1 字节 | 1 字节 |
| KB | Kilobyte | 1024 B | 1000 B |
| MB | Megabyte | 1024² B | 1000² B |
| GB | Gigabyte | 1024³ B | 1000³ B |
| TB | Terabyte | 1024⁴ B | 1000⁴ B |
| PB | Petabyte | 1024⁵ B | 1000⁵ 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 参数
| 符号 | 英文 | 数值 | 说明 |
|---|---|---|---|
| M | Million | 10⁶ 百万 | 小模型,几百 M 参数 |
| B | Billion | 10⁹ 十亿 | 主流,7B/14B/70B |
| T | Trillion | 10¹² 万亿 | 超大规模模型 |
| P | Peta | 10¹⁵ 千万亿 | 仅理论,无实际商用模型 |
再往上 E、Z、Y 这些,完全不会出现在大模型参数场景,不用管。
换算示例(参数数量 → 中文数量级)
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。
两者关系
- wtforms:底层核心库,框架无关,纯 Python 表单校验、渲染逻辑,可以用在任何 Python 项目(Django、Tornado、原生 Python 都能用),不依赖 Flask。
- flask-wtf:对 wtforms 的 Flask 专用扩展,基于 wtforms,做了 Flask 适配,增加 Flask 专属能力。
核心对比
| 项目 | wtforms | flask-wtf |
|---|---|---|
| 依赖 | 无 web 框架依赖 | 依赖 wtforms + flask |
| CSRF 保护 | 没有 | ✅ 内置 CSRF 令牌(Flask session) |
| 表单提交 | 手动拿 request 数据 | form.validate_on_submit() 一键封装 request |
| 文件上传 | 基础文件字段 | 封装 FileField,集成 Flask 上传校验 |
| Recaptcha 验证码 | 无 | 自带 reCAPTCHA 字段 |
| 导入写法 | from wtforms import Form, StringField | from flask_wtf import FlaskForm(继承 FlaskForm 而不是 wtforms.Form) |
4. Python 依赖注入(injector)
4.1 最简自动注入(无 Module)
只要加 @inject + 类型注解,injector 自动构造依赖树。
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 写法
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,递归把它所有依赖全部自动填好。
看这段:
class UserService:
@inject
def __init__(self, db: DBConn):
self.db = db
inj = Injector([AppModule()])
svc = inj.get(UserService)手动写等价代码(injector 内部帮你干的活):
# 框架内部逻辑伪代码
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),就是写绑定规则的地方。
class AppModule(Module):
def configure(self, binder):
binder.bind(IDB, to=RealDB) # 配置:IDB → RealDB当你创建容器:
injector = Injector([AppModule()])容器会自动执行 AppModule().configure(binder),把所有绑定规则注册进容器。
Module 带来的好处
- 解耦切换实现:生产用 AppModule 绑定 RealDB;单元测试换 TestModule 绑定 TestDB。UserService 业务代码一行不动。
- 集中管理依赖绑定:项目大了,几十上百个接口绑定,全部收拢到各个 Module,不要散落在业务代码。
- 可以多个 Module 传给
Injector([M1(), M2()]),模块可以拆分。
没有 Module 能不能绑定?可以,Injector 第二个参数接收 binder 回调:
inj = Injector([], lambda binder: binder.bind(IDB, to=RealDB))但是项目大了,写 lambda 很乱,所以正式项目都封装成 Module 类。
4.5 binder 的语法是固定的吗?
binder 是 configure 方法传入的 Binder 对象实例,方法名是库固定的 API,不能自己随便改;但是调用方式可以灵活。
binder 核心常用 API(固定)
# 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(工程标准写法,你的示例)
class AppModule(Module):
def configure(self, binder):
binder.bind(IDB, to=RealDB)写法 B:不继承 Module,直接传回调,适合小脚本
def configure_binder(binder):
binder.bind(IDB, to=RealDB)
injector = Injector([configure_binder])可以传函数,不一定非要继承 Module 类。Module 本质就是一个方便封装的语法糖。
4.6 把整个流程串一遍,理解发生了什么
injector = Injector([AppModule()])- Injector 初始化,遍历传入的模块列表
[AppModule()] - 如果是 Module 实例,调用它的
.configure(binder),把 binder 对象传进去 - 执行
binder.bind(IDB, to=RealDB),容器内部保存映射关系:IDB → RealDB - 后续执行
injector.get(UserService)- UserService 构造需要
db: IDB - 容器查绑定表:IDB 对应 RealDB → 实例化 RealDB,注入进去。
- UserService 构造需要
如果没有绑定:容器看到依赖 IDB 抽象类,不知道该 new 哪个类,直接抛异常。
5. SQLAlchemy ORM 更新与删除
5.1 更新两种方式对比
# 方式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 删除两种方式对比
# 方式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 装饰器,用生成器函数快速写上下文管理器。
规则:
yield之前的代码 → 等价于__enter__(进入 with 执行)yield→ 切出去执行 with 缩进里面的业务代码;yield的返回值就是as的变量yield之后的代码 → 等价于__exit__(离开 with,无论报错与否,都会执行)
⚠️ 重点!with 缩进里的业务代码就是运行在 yield 暂停的间隙!
简化演示
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("外部")输出:
① enter, 进入 with
with 内部, v=666
② exit, 离开 with, 一定执行
外部6.2 回到 auto_commit,拆解执行流程
@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"执行时序:
- 调用
db.auto_commit(),进入函数,走到yield,暂停 - 执行 with 缩进:查询、修改对象
- with 块执行完毕,回到
yield后面,运行self.session.commit() - 如果 with 内部抛出异常,直接跳入
except分支,执行rollback,重新抛出异常
教程 PPT 文字错误回顾:
"我们可以在 yield 之前对数据进行操作(新增、修改、删除)"
❌ 错!业务代码是写在 with 缩进块,运行在 yield 暂停期间,不是写在 yield 前面。yield 前面是初始化阶段。
7. 大模型训练:预训练与后训练
核心对比表
| 维度 | 预训练 Pre-training | 后训练 Post-training |
|---|---|---|
| 核心作用 | 获取知识,决定能力天花板 | 对齐人类,决定能力怎么表现出来 |
| 数据特点 | 海量无标注原始文本 | 少量高质量指令、偏好标注数据 |
| 训练任务 | 预测下一个 token | 指令跟随、人类偏好对齐 |
| 算力成本 | 极高 | 轻量很多 |
| 产出 | 基座 Base 模型 | 可直接对话的 Chat 模型 |
| 能不能新增知识 | 主要在这里注入知识 | 几乎不新增知识,只改变输出行为 |
完整流程链路
海量原始文本
↓
【预训练】→ 基座模型(只会续写,不会聊天)
↓
【后训练】
├── SFT 监督微调
└── RLHF / DPO 偏好优化
↓
可直接对外使用的对话大模型重要误区:后训练几乎不会给模型增加新知识,只是改变它怎么输出答案;模型懂多少,主要是预训练阶段决定的。
8. LangChain 中的 Chain
8.1 Chain 基本介绍
在许多编程语言和库中,Chain 通常用来描述一系列的操作或函数,这些操作或函数按照特定的顺序依次执行,前一个操作的输出会作为后一个操作的输入。这种模式也被称为管道(Pipeline)或链式调用(Chain Calling)。
而在 LangChain 库中,也有 Chain 的概念,用于在复杂场景下,将 LLM 组件、提示词模板、向量存储、记忆、输出解析器等多个组件串联起来一起使用,在 LCEL 表达式出现之前,LangChain 就为 Chain 设计了相应的接口,并且为不同场景设计封装了大量 Chain 组件。
在 LangChain 中,存在两种类型的链:
- [推荐] 使用 LCEL 构建的链(顺序可执行链);
- [遗产] 通过 Chain 类子构建的链,这些链不使用 LCEL,而是独立的类。
例如下方是 0.1.0 版本之前的 Chain 基类的定义:
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,而不需要额外的处理步骤,使用示例如下:
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]):
② 欧式距离衡量向量之间的直线距离,得到的值可能很大,最小为 0,通常用于低维空间或需要考虑向量各个维度之间差异的情况。欧氏距离较小的向量被认为更相似,欧式距离的计算公式如下:
9.2 严谨的知识点(考试 / 面试要注意)
- Faiss 本质是检索算法库,不是数据库:
- 没有元数据管理、没有集合、没有删除更新、没有并发控制、没有事务,只做向量相似度搜索;元数据、文档内容需要你自己另外保存维护。
- Milvus、Weaviate、Pinecone 才是真正意义完整向量数据库。
- Annoy 同样也是本地库(非服务),原文把 Annoy 放到"本地部署 API 向量数据库"是一个小错误:Annoy 和 Faiss 一样,只是本地库,不提供网络 API 服务,不能通过网络请求访问,不应该跟 Milvus、Weaviate 放一类。
三类整理修正版
| 分类 | 特征 | 正确例子 |
|---|---|---|
| 本地文件(嵌入式库) | 无独立服务进程,程序内调用,磁盘存索引文件 | Faiss、Annoy、Chroma(嵌入式模式) |
| 本地部署 API 向量数据库 | 本地启动独立服务,提供 HTTP/gRPC 网络 API | Milvus、Weaviate、Qdrant |
| 云端 API 向量数据库 | 托管云服务,只调用远程 API | Pinecone、TCVectorDB |
原文小 bug:Annoy 放错分组;Faiss 在入门教学叫"本地文件向量数据库"方便理解,但记住严谨表述是:向量检索库,不是完整向量数据库。
快速记忆
- Faiss:库,嵌入进程,本地文件保存索引,无网络接口
- Milvus:服务,独立进程,本地部署,网络 API 访问
- Pinecone:云托管,完全云端,只调用 API
9.3 常见的切分策略
| 策略 | 适用场景 | Chunk大小 |
|---|---|---|
| 按字符数切分 | 通用场景 | 200-500字 |
| 按段落切分 | 结构化文档 | 自然段落 |
| 按语义切分 | 技术文档 | 完整的概念单元 |
| 滑动窗口 | 连续性强的内容 | 重叠50-100字 |
所以,流程图中的「步骤3:提取相关片段」并不是从完整文档中提取内容,而是从向量数据库中取出检索到的 Chunk 的原始文本。文档在存储时就已经切分好了。
9.4 多路召回(Hybrid Retrieval)
+----------------+
| 用户问题 |
+----------------+
|
+-----+-----+----------+
v v v v
向量 关键词 BM25 时间过滤
检索 检索 检索 (最近更新)
| | | |
+-----+-----+----------+
v
合并 + 去重
|
v
Rerank 重排序
|
v
Top-K 结果这就是「多路召回」的核心思想:让不同的检索方法各显神通,最后综合决策。就像组建一个专家团队,每个专家擅长不同领域,共同给出最佳答案。
10. 术语读音与词源(anvil / 砧 / Pinecone / Milvus)
10.1 anvil
英 /ˈænvɪl/
名词 n.
- 铁砧;砧子(铁匠打铁垫的铁块)
短语:on the anvil 在研讨中;在制作中
10.2 砧
读音:zhēn(第一声)
不要读成 zhàn
释义:
- 捶、砸东西时垫在底下的器具:铁砧(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/ | 米尔-沃斯 | 鸢(猛禽) |
补充小知识点(面试)
- Pinecone:海外主流云原生向量 DB,纯云端托管 SaaS,没有本地部署版本,只提供 API,对应文中「云端 API 向量数据库」。
- 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 分两大块:
- ORM 层(大家最常用):对象关系映射,类映射数据表,
User.query()/session操作,对应 ORM 概念。 - Core 层(底层 SQL 抽象):不做对象映射,只是帮你用 Python 语法拼 SQL 语句,返回行数据,属于 SQL 构建器,不用 ORM 也可以单独用 Core。
12. MySQL 索引:DDL 与使用
12.1 创建表时就建立索引
-- 创建表同时建普通索引
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 表已经存在,事后新增索引(最常用)
-- ✅ 普通索引
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 删除索引
DROP INDEX idx_user_phone ON user;12.4 查看一张表有哪些索引
SHOW INDEX FROM user;12.5 验证 SQL 有没有用上索引:explain
在 select 前面加上 explain,看 type 列:
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)
-- ✅ 不需要回表,查询字段全部在索引内
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 (主键) | name | age | phone |
|---|---|---|---|
| 1 | 张三 | 22 | 131xxxx |
| 2 | 李四 | 35 | 132xxxx |
| 3 | 王五 | 28 | 133xxxx |
如果给 phone 建立索引:数据库单独构建 B+ 树,树上存一份 phone 值 → 行位置。
select * from user where phone='133xxxx';- 无索引:遍历全部行,逐行对比 phone。
- 有索引:在 B+ 树快速找到
133xxxx,直接定位到对应的那一行。
13.5 什么是回表
前提:只针对 MySQL InnoDB(最常用)
- 主键索引 = 聚簇索引:B+ 树叶子节点,存放完整的一整行数据。整张表的数据,就放在主键索引树上。
- 普通二级索引:B+ 树叶子节点,不存完整行,只存【索引字段的值 + 主键 id】。
回表:通过二级索引查到主键 id,再拿着这个 id,去主键索引里面查询拿到完整行数据,这个二次查找动作就叫回表。
举例
表 user
| id (主键) | name | phone |
|---|---|---|
| 1 | 张三 | 1310000 |
| 2 | 李四 | 1320000 |
我们给 name 建立普通二级索引 idx_name(name)。
二级索引(idx_name)B+ 树叶子存的内容:
"张三" → id=1
"李四" → id=2⚠️ 这里没有
phone字段!二级索引只存 name 和主键 id。
执行 SQL:
SELECT * FROM user WHERE name = '张三';完整流程(发生回表)
- 在二级索引
idx_name搜索,找到 "张三",拿到主键id=1 - 二级索引里面只有 name 和 id,没有 phone!拿不到
*全部字段 - 拿着
id=1,去主键聚簇索引 B+ 树,读取完整一行 (id,name,phone) 👈 这一步就是回表 - 返回完整数据
回表本质:两次 B+ 树查找。第一次走二级索引,第二次走主键索引。多一次 IO,性能会变差。
13.6 覆盖索引是「查询效果」,不是索引种类
容易混淆的通俗说法
很多教程口语会说「建一个覆盖索引」,这是口语简写。
真实含义是:建一个联合索引,让它能够满足某条 SQL 的全部查询列,使得这条 SQL 可以触发覆盖索引现象,并不是数据库有一种叫「覆盖索引」的新索引。
再回顾 InnoDB 二级索引叶子存储内容
二级索引叶子 = (索引列的值, 主键 id)
- 如果
select取出的字段,全部属于索引列 → 直接从叶子拿数据,不走回表 → 覆盖索引 - 如果要查索引以外的列 → 拿主键 id,回表去聚簇索引读取完整行
一句话总结
覆盖索引是查询的效果,不是索引的种类; 底层还是普通 / 联合索引,查询语句刚好可以全部从索引拿到数据,才成为覆盖索引。
13.7 小对比表
| 名称 | 是索引类型? | 说明 |
|---|---|---|
| 主键索引(聚簇索引) | ✅ 是 | 物理真实索引 |
| 普通二级索引 | ✅ 是 | 物理真实索引 |
| 唯一索引 | ✅ 是 | 物理真实索引 |
| 联合索引 | ✅ 是 | 物理真实索引,多字段索引 |
| 覆盖索引 | ❌ 不是 | 查询行为、优化现象,依赖上面真实索引实现 |
补充:Oracle 里面叫索引扫描(index only scan),PostgreSQL 也是叫 index-only-scan,字面意思就是:只扫描索引,不去访问数据表,和 MySQL 覆盖索引是同一个东西。
13.8 使用注意(踩坑点)
- 必须类型注解:
@inject靠类型注解解析,无注解无法自动注入。 @singleton作用域仅限同一个 Injector 实例,新建 Injector 会重新创建对象。- 循环依赖会抛异常,需要重构代码。
- 不要全局单例 Injector;一般应用启动构建一次 Injector。
NotBoundError:类型没有绑定,要么自动可实例化,要么在 Module 里显式 bind。
13.9 知识小问答:@inject 到底什么时候发生注入?
问:是不是调用 injector.get(X) 才发生注入?
答:是的,真正做依赖注入、实例创建,发生在 injector.get(SomeClass) 运行时。
@inject 装饰器本身只是打标记,不会立刻做任何实例化。
@inject:只是给函数 / 方法对象上贴一个元数据标记,告诉 injector:「这个方法的参数,需要容器来提供」,此时完全不创建对象,不解析依赖。- 只有执行
injector.get(Cls)的时候,容器才:- 读取类的构造函数,看到
@inject的标记 - 读取参数的类型注解
- 递归去容器拿到每一个依赖对象
- 调用构造函数,把依赖传进去,生成实例
- 读取类的构造函数,看到
关键点:
@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⁻⁹(十亿分之一)。
常用单位:
- nanometer (nm) 纳米:10⁻⁹ 米,微观尺度
- nanosecond (ns) 纳秒:10⁻⁹ 秒,芯片 / 高速通信用
- nanogram (ng) 纳克:微量质量单位
衍生词汇:nanotechnology 纳米技术、nanoparticle 纳米颗粒
二、Linux/macOS 终端 GNU Nano 编辑器(程序员最常指)
定位:Linux 命令行极简文本编辑器,对标 Windows 记事本,新手首选,替代复杂的 Vim/Emacs,绝大多数系统预装。
20. Celery 启动命令(imooc-llmops)
启动 worker 前先确认 Redis 在跑:
redis-cli -a llmops123456 ping # 返回 PONG 就好启动 Celery worker:
cd /Users/guowangyang/Downloads/imooc-llmops/api
celery -A app.http.app.celery worker -P solo -c 1 --loglevel INFO用虚拟环境里的 python 启动:
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 个里面做随机采样。
完整执行流程
- 解码器输出 logits → 经过 Softmax 归一化得到 0~1 概率总和 = 1 的概率分布;
- 对所有 token 概率降序排序;
- 截取前 Top K 个最高概率 token,其余 token 概率清零;
- 对保留的 K 个概率重新归一化;
- 按新概率随机抽取一个作为当前输出 token;
- 循环迭代生成下一个 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 采样。
执行流程
- 同样 Softmax 得到全量 token 概率;
- 降序排列 token;
- 依次累加概率,直到总和 ≥ 设定的 P 值(如 0.9);
- 只保留参与累加的这部分 token,其余清零;
- 归一化后随机采样输出 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:有什么不同?
两者都控制采样,但方式不同:
| 维度 | Temperature | Top-p |
|---|---|---|
| 作用对象 | 调整所有词的概率:低温强化高概率词、压制低概率词;高温拉平概率分布 | 只选择头部的词:低 p 只考虑最可能的几个词,高 p 考虑更多词 |
| 动态性 | 固定地调整分布,不管概率分布情况如何,调整方式都一样 | 动态地选择候选集:模型很确定 → 候选集小;模型不确定 → 候选集大 |
| 取值示例 | —— | P=1.0 取所有词(等于没限制);P=0.9 只取累积概率到 90% 的词;P=0.5 只取累积概率到 50% 的词 |
21.6 采样流水线的完整顺序
1. 模型输出原始 logits
2. Temperature 温度缩放(最先执行)
3. Top-K 截断过滤
4. Top-P(Nucleus)核采样截断过滤
5. 可选:重复惩罚、最小概率阈值等其他修正
6. Softmax 归一化概率分布
7. 随机采样选出下一个 token22. 编译型 / 解释型 / JIT 关键概念区分
- 源码编译成机器码:C/C++、Go、Rust → 编译型
- 源码编译成字节码,虚拟机解释字节码:Python、Java
- Java 有 JIT,会把字节码热点编译机器码
- 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 宿主机端口:容器内部端口
-p A:B
A = 你电脑(宿主机)的端口
B = docker 容器里面程序监听的端口拿 Weaviate 的例子:
8080:容器内部 Weaviate REST 服务固定端口,写死不能改,容器里程序就监听 8080。8081:你本机电脑上对外暴露的端口,这个数字你可以随便改。
23.1 举两个例子
原版:
-p 8080:8080本机 8080 → 映射到容器的 8080,访问
http://127.0.0.1:8080如果本机 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 K3 | DeepSeek V4-Pro |
|---|---|---|
| 总参数量 | 2.8T(更大) | 1.6T |
| 激活参数 | 约几十 B | 49B |
| 行业地位 | 全球第一开源模型 | 全球第二大开源模型 |
| 优势 | 多模态能力更强 | 推理成本极低、代码 / 中文能力突出 |
25. 从 CloudAgent 沙箱把文件搬到本机
25.1 背景:为什么不能直接下载
tdocs-cloudagent 控制台的 /e2b/file 接口把响应当文本处理:
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 搬运(不改代码,临时用)
① 沙箱里编码
cd /workspace/ppt_one/output
base64 -w0 single-digit-1.pptx > /tmp/f.b64
# -w0 = 不换行,输出单行;不加会每 76 字符插一个 \n
wc -c /tmp/f.b64 # 25251 字节 → 约 33668 字符② 复制前先确认没被截断
tail -c 200 /tmp/f.b64zip 类文件的 base64 结尾会带 UEsFBgAAAA...,这是 PK\x05\x06 (End of Central Directory)。能看到它就说明文件尾完整。
③ 复制到本机
33KB 一般能一次贴完。建议先用 pbpaste 从剪贴板落盘,避免终端自动换行:
# macOS:从剪贴板取,顺手去掉可能被掺入的换行/空格
pbpaste | tr -d '\n\r ' > /tmp/f.b64
wc -c /tmp/f.b64 # 应该 ≈ 33668④ 本机解码
⚠️ macOS 的 base64 不接受位置参数,下面这种 Linux 写法会报 base64: invalid argument:
base64 -d /tmp/f.b64 > out.pptx # ❌ macOS 不支持正确写法(二选一):
# 方式一:-i 指定输入
base64 -d -i /tmp/f.b64 > ~/Downloads/single-digit-1.pptx
# 方式二:stdin 重定向(Linux / macOS 通用)
base64 -d < /tmp/f.b64 > ~/Downloads/single-digit-1.pptxmacOS 的 base64 用法是 base64 [-Ddh] [-b num] [-i in_file] [-o out_file], 输入必须走 -i 或 stdin。
⑤ 验证(这步不能省)
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.pptxunzip -t 才是判据:
| 输出 | 含义 |
|---|---|
No errors detected | ✅ 完整,能正常打开 |
missing bytes / zipfile is truncated | ❌ 复制被截断,要分段重搬 |
更稳的替代(python 容错更好,会忽略空白):
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 改成读原始字节:
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,不用额外加。)
重启后一条命令下载:
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.pptxsid 从控制台 Connect 后的 /connect 响应里拿,或浏览器 Network 面板看 /e2b/* 请求的 X-Session-Id。
25.4 只有大文件(>100KB)才需要分段
# 沙箱
split -b 3000 /tmp/f.b64 /tmp/part_
ls /tmp/part_*
# 逐个 cat 复制,本机按顺序追加
pbpaste | tr -d '\n\r ' >> /tmp/f.b6425.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 登录与密码认证
redis-cli已进入交互式页面,输入
auth <密码>还没进入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 是一个"接收输入、返回一个词"的函数
第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 / LSTM | 2010 早期 | 维护持续更新的隐藏状态,逐词串行传递记忆;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 = 恰好有一个位置是激活(热),其余全部关闭(冷)。
类别:[猫, 狗, 鸟]
猫 → [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 自监督学习:答案在原文里
原始文本"人工智能是计算机科学的一个分支",每个位置自动生成一道填空题:
填空题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 镜像常用命令
docker images # 查看所有镜像
docker pull 镜像名:tag # 拉取镜像
docker rmi 镜像ID/镜像名:tag # 删除镜像
docker rmi $(docker images -q) # 删除全部镜像(谨慎!)
docker search xxx # 搜索镜像41.2 容器日志命令
docker logs 容器名/ID # 查看容器日志
docker logs -f 容器名/ID # 实时滚动看日志
docker logs --tail 100 容器名/ID # 看最后 100 行41.3 排错速查表
| 现象 | 处理 |
|---|---|
| 容器名字已占用 | docker ps -a 找到残留容器,docker rm 名字 |
| 端口被占用 Bind failed | 改 -p 本机端口:容器端口,左边数字换别的 |
| 容器启动后立刻退出 / Restarting | docker 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.3 的 VectorIndexType 枚举只定义了三类:hnsw、flat、dynamic。但 Weaviate Cloud 上的 Dataset collection 用的是 hfresh 索引类型(新的 HNSW+Flat 混合索引,结合 HNSW 搜索速度和 Flat 精确度)。旧版客户端不认识这个类型,VectorIndexType('hfresh') 构造时报 ValueError。
42.2 修复内容
- 升级
weaviate-client:4.18.3→4.22.0(hfresh已加入枚举) - 修复 deprecated API:
connect_to_wcs()→connect_to_weaviate_cloud()(参数签名完全一致,只是方法名变了)
42.3 其他文件提醒
同目录还有 15 个文件也用了已废弃的 connect_to_wcs(不至于报错,只有 DeprecationWarning)。后续运行那些示例时建议批量替换为 connect_to_weaviate_cloud。
43. 云业务的三个特性
往下讲之前先打个预防针:我们能拿到这些结果,因为我们是一个比较典型的云业务,有三个特征:
- 不是超级 App,由很多子产品组成——比如云原生下面就有 TKE、TKE Serverless 一大串——每个子产品就是几十人的小团队,适合从点上创新;
- 底层通常基于开源,数据库、Kubernetes、Hypervisor、存储、网络的 know-how 在模型训练时基本内化过,对我们这类业务天然更熟;
- 技术就是产品,虽然也要解决商业化,但只要聚焦技术突破,产品竞争力就能提升。
这三个特征让 AI 提效在我们这里效果特别显著,别的业务未必能直接复制。
44. 新的技术必须匹配新的生产模式
一个基础判断:新的技术必须匹配新的生产模式
45. AI 打破的三个旧假设
过去整套研发体系建立在三个假设上:
- 人的能力有边界,所以必须分工协同——AI 让一个人的能力变强了,分工的必要性还继续存在吗?
- 执行和试错成本高,所以必须先规划——但认知是在实践中积累的,规划又必须在实践之前做,等于在认知最缺乏的时候做最重要的决策,本身是个矛盾。AI 极大降低了试错成本,也许可以先做出来再修正。
- 人是独特的,经验只能靠培养和传帮带——AI 可以把经验复制,一个调通的 Agent 可以复制一万份、一亿份,组织可以大量并行。
这就产生了我们团队最核心的一条认知:
AI Agent 不是工具,是新的生产主体;AI 提效不是代码补全,是生产模式的重塑。
46. 超级个体,消除协同损耗
人的协同是必要的,但绝大多数低效都来自协同。做一个产品从想法到发布,要拉前端、后台、运维、产品、策划;两三个人开会信息就开始有偏差,人一多是指数级上升;每个人只对自己的岗位职责负责,不对最终结果负责,信息衰减、循环等待、责任稀释一层层叠加。
47. sheetagent:GAS shim 与 run_command 调用链
GAS = Google Apps Script,Google 表格的脚本 API。它的标志性写法是:
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.ts | Worker 线程跑脚本 → 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 文件改 XML(xl/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 用的方法名:
channel.sendSync({ op: "ctrl.log", args: { level, msg } });
// ↑ 注意是 SyncsendSync 内部是 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 时传了什么:
new Worker(workerPath, { workerData: { sab, script, fileId, sheetId } })只有共享内存、脚本、两个 id——没有 api。因为 Node 跨线程传数据走结构化克隆,克隆不了 HTTP 连接、socket、函数,更别说 UpstreamClient 里那个 MCP 长连接(StreamableHTTP 会话)。真传就得每个脚本重建一次 MCP 握手,代价太大。所以 api 只能留在主线程,worker 只能"委托"。
② 并发闸门必须集中
bridge.ts 里这个常量和它的注释,是我见过最实在的架构注释:
const READ_CONCURRENCY = 4;
// 线上出现过一次 12 万行 getValues 扇出 237 路、把 sheetengine 打到 502/503 的事故。
// 不设限就是拿自己的读请求打自己的服务端。如果每个 worker 自己发请求,这个闸门就形同虚设(N 个脚本并发 = 无上限扇出)。集中到主线程,才能统一限流、去重、批处理。
③ 沙箱安全边界
worker.ts 第一行 import vm from "node:vm"——脚本跑在 vm 沙箱里,能看到的全局对象只有 buildShimGlobals() 注入的那些(SpreadsheetApp / Sheet / Range…),碰不到 fs、net、process。而它唯一能对外产生影响的出口,就是 protocol.ts 里的 OpName 白名单。
这是个很干净的安全模型:脚本再怎么折腾,也只能发白名单里的 op,出不去这个圈。
47.2.3 那 worker 线程的作用到底是什么?
四个,缺一不可:
- 提供一个"可以被安全冻死"的线程 —— 它阻塞时,主线程照样跑事件循环、处理网络、响应 MCP 心跳。这是最核心的作用。
- 沙箱隔离 —— vm + 只注入 shim,脚本碰不到宿主资源
- 崩溃/死循环隔离 —— 脚本死循环可以
terminate()掉;host.ts里专门处理worker.once("exit")提前退出的情况,主进程不受牵连 - 让"同步 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.ts | implements SheetApi → 转发到本地 editor 39099 |
api/remote_api/client.ts | implements SheetApi → 转发到远端 sheet-mcp |
runtime/bridge.ts | handleOp 里拿 ctx.api 调真实实现 |
server.ts | 按 SHEET_API_MODE / PLATFORM 决定注入哪一套实现 |
一句话:它是「工具层 ↔ 后端实现」之间的编译期契约。工具想调什么,先得在这个接口里声明过。
2. runtime/protocol.ts —— 通信协议(run_command 通道用)
它定义 OpName 联合类型 + OpRequest / OpResponse 消息格式。消费者:
| 谁 | 怎么用 |
|---|---|
runtime/invoke.ts | makeInvoke() 构造 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.ts) | ✅ sheetUnsetFreeze 已声明 | 不经过 | 能取消冻结 ✅ |
| 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/ 里 Sheet 和 Range 两个类的方法全集(脚本里能直接调的):
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 个 op | N 次调用 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 的直接含义
收益定性要修正:这批工具的收益主要是成功率和稳定性(把"模型写对公式"变成"填参数"),不是"能力从无到有"。只有色阶/数据条/图标集/命名视图/钻取这几项是真正新增的表达能力(靠 TS 层拼),其余是路径优化。
当前收益 = 0:白名单没同步 → 子代理只能 run_command → 新工具一次都调不到。
同步白名单后,必须给 prompt 加路由规则,否则模型会继续无脑 run_command(它有路径依赖)。建议写法:
场景 走谁 单条条件格式 / 色阶 / 数据条 / 冻结 / 数据验证 / 单条筛选 专用工具 需要遍历判断、批量 N 条、多步编排、先读后写 run_command 工具报错且明确是"引擎不支持" 不重试,直接判未实现 顺带要改的:
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 实测片段):
// ---- 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) | 30 | TS 层模拟(分段/辅助列) | 真原生对象,color_scale.ts + formula_viz.ts 可整体删掉 |
| P1 | 浮动图片 op(drawing 层) | 5 | Python shim 改 OOXML | 原生 |
| P2 | 工作簿设置 | 9 | Python shim | 原生 |
| P3 | 评论批注 | 4 | Python shim | 原生 |
| P4 | 名称管理器 | 3 | Python 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 立刻从"模拟"变"原生通过",不用等引擎。
要我现在做哪个?
- A(推荐,快):用 Python 写原生
<colorScale>/<dataBar>OOXML + openpyxl 回读验证,出一份实测报告 → 作为给引擎团队的需求单附件,同时可升级caps_cf.py - B:在引擎源码里加一个 op 原型(比如先加
sheet_color_scale),产出 diff 给引擎团队评审(本地不编译) - C:先不动,我把 51 条 A 类需求整理成一份带 OOXML 节点规范 + 现有代码位置的需求清单
我倾向 A——它是唯一能"立刻看到效果"的路径,而且产出直接服务于推引擎。