长上下文现在几乎成了大模型应用的标配能力,但真正到了工程落地阶段,你会发现"能用"和"好用"之间隔着一道巨大的显存墙。我在给一个检索增强项目做长文档问答时,把上下文从8K拉到32K,GPU显存占用直接翻了六倍多,推理延迟也从单token几十毫秒变成了几百毫秒。当时第一反应是"是不是Attention算不过来了",把FlashAttention、Triton算子轮着试了一遍,效果有但没到质变。后来才醒悟:长上下文场景下,真正让你寸步难行的不光是Attention的计算量,还有KV缓存的存储量,以及随之而来的内存管理、缓存失效、跨页调度一堆问题。
这篇文章我就围绕Attention、压缩、缓存、跨页推理这四条线,把长上下文技术怎么从底层支撑起"读得完、记得住、推得动"这件事拆开讲。会涉及FlashAttention这类算子的优化思路,也会讲到vLLM这类推理框架的PagedAttention是怎么借鉴操作系统虚拟内存技术来解决KV缓存碎片化的,最后结合我自己在长文档问答、多轮对话、Agent任务里的实测经验,聊聊选型和调优时真正值得关注的坑。内容偏底层但不需要你有硬件背景,我会把复杂度、显存账、策略取舍都算给你看。
1. 长上下文为什么吃显存:先把Attention和KV缓存的账单算清楚
很多人一提到长上下文就想到Attention的平方复杂度,觉得这是唯一瓶颈。但实际上,一个128K上下文的模型跑推理时,Attention计算的耗时增长和显存中KV缓存的存储增长是两个完全不同量级的问题。前者是"慢",后者是"装不下"。搞清楚这笔账,你才能理解为什么行业里会搞出压缩、缓存、分页这一堆花活。
1.1 每多一个token,你要多算多少乘法
我们先看一次前向推理中最核心的计算:Query和所有Key做点积。假设当前序列长度是L,每个头的维度是d,那么单个头在计算注意力分数时,要算L×L次点积,每次点积大概是d次乘法。推到整个模型层面,总的计算量大约正比于L²。这就是论文里常说的"平方复杂度"。
举例说,序列长度从4096涨到8192,计算量不是翻倍,而是翻四倍;涨到32K,相对4K是64倍。这个增长速度非常吓人。我曾在一个7B模型上做过纯计算耗时测试:4K上下文时单次prefill大约120ms,改成16K后直接变成1.8秒左右,增长了15倍,而序列长度只增加了4倍。
不过要注意,"平方增长"针对的是prefill阶段——也就是你要把一整段上下文喂给模型做并行计算的那个阶段。这个阶段你有GPU的大规模并行能力可以依赖,所以L²的增长虽然猛,但你还能靠更极致的并行和算子优化去硬扛。
1.2 KV缓存是怎么出现的:用空间换时间的经典套路
到了decode阶段(也就是逐token生成回答的阶段),每次只生成一个token,按说计算量变小了,但Attention里有一个必须维护的东西:当前token的Query,要和历史所有token的Key、Value做交互。如果每次生成都重新算一遍历史token的K和V,那等于把prefill阶段的工作全部重复一遍,时间开销完全不能接受。
于是大家开始用典型的"空间换时间":把历史上每个token经过注意力层之后得到的Key向量和Value向量缓存下来。这个缓存就是KV Cache。每生成一个token,就把新token的K和V追加到缓存里。下一轮生成时,只需要拿着当前token的Q去和缓存里所有的K做点积,再和所有V加权就行,省掉了对历史token的重复计算。
这套机制本身设计得很美,但它有一个物理上的硬代价:存储。每个token在每个注意力层、每个注意力头都要存一组K和V,层数一多、头数一多,这个存储量直接线性膨胀。
1.3 账单汇总:4096和128K不是量变,是质变
给一个具体的账本。假设你的模型是7B参数、32层、40个注意力头、每个头维度128。算一下每个token需要多大KV:2(K和V)× 32层 × 40头 × 128维 × 2字节(FP16)= 640KB。注意,这是一个token的KV缓存占用的显存。
然后你乘上序列长度:4K上下文时,KV缓存大约是2.5GB;32K上下文时是20GB;128K是80GB。一个7B模型本身权重占14GB(FP16),80GB的KV缓存意味着你需要两张80GB的A100才能勉强把KV放下,这还没算中间激活值和计算临时内存。这还只是7B,如果是70B模型,每token的KV会更大,128K基本上需要数张H100。
这就是问题的全貌:Attention计算的平方复杂度和KV缓存的线性增长叠加在一起,让长上下文成了"算力黑洞+显存黑洞"的双重消耗。理解了这笔账,再看后面的压缩、缓存、跨页推理,都是在不同的环节对冲这两笔开销。
2. Attention层优化:FlashAttention不是银弹,但它是必修课
既然Attention的平方计算是长上下文的第一道坎,自然有人专门优化它。最典型的是FlashAttention。我不打算把论文公式复述一遍,而是讲清楚它为什么快,以及在长上下文里的真实收益边界。
2.1 标准Attention是怎么一层层算出来的
标准Attention的实现逻辑是:先把Q、K、V三份矩阵准备好,然后算Scores = Q @ K.T,得到L×L的分数矩阵;对这个矩阵做softmax;再用softmax的结果去乘V,得到输出。
这个流程的数学是干净的,但工程上是浪费的。问题在于,中间那个L×L的分数矩阵会被完整写到HBM(高带宽显存)里,然后再读回来做softmax,最后再读一次做矩阵乘。HBM的带宽远低于芯片内部的计算速度,于是大量时间花在了数据的搬进搬出上。序列越长,中间矩阵越大,读写压力越重。这就是"内存墙"问题。你查性能剖析数据时会看到,Attention算子的SM利用率并不低,但整体耗时下不来,多半是HBM带宽打满了。
2.2 FlashAttention的IO感知思路:能不落盘就不落盘
FlashAttention的核心思路特别朴素:不要让大矩阵落到HBM里,而是把它切成小块,让它在SRAM(芯片上的高速缓存)里完成softmax和与V的乘法。用学术一点的话说,这是一个"IO感知"的算法,它重排了计算顺序,同时做了一点数值技巧(在线softmax),使得我们不需要等到完整分数矩阵算完才能做归一化。
我实际测过在580和4090上的表现:在8K上下文的prefill阶段,FlashAttention相比PyTorch原生attention实现有1.6到2.5倍的加速,而且峰值显存占用有明显下降,因为不用分配那张L×L的分数矩阵了。对于做推理服务的人来说,这几乎是必选项。很多框架默认就开了FlashAttention,比如HuggingFace Transformers里设置attn_implementation="flash_attention_2"就行。
但你要记住:FlashAttention处理的是"计算时内存搬运"问题,它不能压缩KV缓存本身。序列特别长时,你的瓶颈是KV缓存能不能装进显存,而不是Attention算子快不快。所以FlashAttention解决的是Attention这一项,KV存储问题得靠别的手段。
关于社区里热门的SageAttention、FlashAttention 4这类东西,我的判断是:算子和算法在持续迭代,但边际收益在递减。例如SageAttention这种引入量化思路去降低Attention计算量的方案,在某些场景能再给你带来20%-30%的收益,但随之而来的数值精度问题需要你在业务里仔细评估。我的建议是,先保证FlashAttention这种基础优化开启,再去做更激进的算子替换。
2.3 稀疏注意力和线性注意力:理论美好,工程难做
除了优化算子的执行效率,还有一条路是降低Attention本身的复杂度。理论上稀疏注意力可以只让每个token关注邻近或者特定位置的token,把复杂度降成O(n)或者O(n log n)。线性注意力则用核技巧把Softmax注意力分解成更便宜的形式。
实话实说,这两类方法在长上下文商业化落地中都没有成为主流。原因很实际:稀疏注意力牺牲了模型对远端信息的感知能力,评测时你可能觉得还行,放到真实任务里,用户问一个需要关联文章开头和结尾信息的问题,模型就开始犯傻。线性注意力改写了注意力分布的性质,训练和推理行为都变了,如果不是从预训练阶段就用这种结构,直接用现成权重做推理会导致严重掉点。
所以我的实操建议是:对于已经训练好的模型,你基本不要去改Attention结构,而是应该在算子执行层面做优化。真正想把上下文做长,后面要讲的压缩、缓存、跨页推理才是在系统层面解决问题的正道。
3. 压缩策略:并不是所有历史都值得一字不差地记住
KV缓存线性增长是长上下文的硬约束。既然存储放不下,那就想尽办法"少存一点"。压缩的本质是:找到那些不影响生成质量的历史信息,把它们裁掉或者变得更紧凑。这里我讲三条实际可行的路线:位置编码改造、提示压缩、状态记忆压缩。
3.1 位置编码与长度外推:先能撑过训练长度再说
很多模型在做长上下文时,第一个"碰壁"点是位置编码。比如用绝对位置编码训练的模型,只要输入超过训练最大长度,位置向量的取值范围就超出模型见过的区间,位置信息会变得混乱。这也是为什么业界后来普遍转向RoPE(旋转位置编码)这类相对位置编码。RoPE本身有不错的外推能力,很多模型训练时就用了某种插值方法,让模型能处理超出训练长度的序列。
实际中,我见到很多团队做长文本评测时,第一件事不是改KV缓存,而是确认自己模型的RoPE是否做了NTK-aware或者YaRN这类插值。如果没做,序列一超过训练时长度,模型的困惑度会突然飙升,你会误以为是显存不够或者缓存出了问题,其实是位置编码在"报警"。
有一个容易被忽视的点:RoPE在推理时也是要参与计算的。当上下文特别长时,位置编码的计算和缓存也要纳入系统设计,不能让它在主循环里反复生成,否则会有不必要的时间损耗。
3.2 提示压缩:从源头减少KV的产生量
很多长上下文场景下,KV缓存里存的大部分Token其实对最终回答没有太大贡献。比如你给模型塞了一堆网页正文、产品文档、聊天记录,里面大量重复的引导语、导航栏、广告文本,模型真正用到的可能是其中一小部分片段。
提示压缩的思路是在把文本送进模型之前,先做一轮抽取、摘要或者结构精简,用更短的文本代替原来的长文本,从而从源头减少KV缓存的产生。这是成本最低的一种"压缩"方式。
我做过一个场景:给模型喂30个网页内容做资料汇总,原始网页加起来大概40K token。我先用一层快速的关键信息抽取,把网页里的正文段落、标题、核心数据提取出来,压缩到8K token,然后只把这8K送进长上下文模型。结果反正评测得分基本没掉,显存占用和推理延迟都大幅下降。这就是典型的"能在外层压缩的就别让模型硬扛"。
3.3 状态记忆压缩:把历史编码成更小的"记忆"
这是更贴近模型内部的一种压缩方式。它不再保留所有历史token的KV,而是让模型把过去的内容总结成一种紧凑的表示。比如MemGPT、Mem0这类记忆层产品,本质上就是在做"关键信息抽取+摘要存储"。它们会把多轮对话的内容归纳成摘要,或者把用户偏好、任务状态整理成结构化的记忆条目,在后面的轮次里再注入模型的上下文。
这种方法的优点是可解释性强、存储成本低,缺点是信息有损。如果摘要抽取得不好,或者记忆条目的设计不合理,模型会丢失关键细节。而且这种压缩往往是任务相关的——换一个任务,原来的摘要方式可能就不适用了。
我的经验是:状态记忆压缩适合对话轮次无限增长但是单轮上下文范围可控的场景,比如客服机器人、Agent的多步任务管理。它不适合那种"我要让模型能检索任意一段历史事实"的场景。后者的正确做法还是把KV缓存管好,而不是压缩掉信息。
4. 缓存机制:KV缓存的生命周期、一致性与共享复用
KV缓存是长上下文系统里最核心的状态。很多人刚开始做推理服务时,只把它当作一个临时变量,用完即丢。但当你开始面对多用户、多请求、多轮对话时,KV缓存的分配、复用、失效策略就变成了一个系统工程问题。这一节我讲KV缓存的生命周期、共享前缀缓存和缓存一致性这三大块。
4.1 KV缓存的生命周期:从扩容到释放
KV缓存的显存分配是动态的。在prefill阶段,你要把整个输入序列的KV全部算出来;在decode阶段,每个token逐步追加。这意味着你不能在请求开始时一次性分配好最终所需的显存,因为不到最后你也不知道生成会持续多长。
大多数推理框架的做法是"预分配+按需扩容":给每个请求预留一部分显存,当KV缓存要超过当前预留空间时,再去申请新的显存块。这个扩容过程如果处理不好,会出现显存碎片和频繁的显存分配开销。我见过一个部署案例,没有限制最大生成长度,结果某个请求疯狂生成长文本,KV缓存不断扩容,最后把同一张卡上的其他请求全部挤爆,整个服务OOM。
这里的工程教训是:一定要给KV缓存设定一个上下限,并且对请求做并发配额管理。别让单个请求无限制地吃显存,必要的时候要敢于拒绝请求或者做降级。
4.2 共享前缀缓存:同一份前缀,别为每个用户存一份
实际业务里有一个非常常见的模式:多个用户共享同一个系统提示词或者同一份文档上下文。比如你做一个客服机器人,所有请求前面都会加上"你是XX平台的客服助手,以下是我们的售后政策:...";或者做一个文档问答,所有人都针对同一份PDF提问。
这种情况下,不同请求的KV缓存里有很大一部分是完全重复的。如果每个请求都独立计算并存储这一份公共前缀的KV,不仅是计算浪费,更是显存浪费。解决思路是共享前缀缓存:把公共部分的KV缓存单独存一份,多个请求按引用指向同一块显存,只对各自不同的后缀部分分配独立KV。
我看到一些团队的实测数据:当请求共享一个长系统提示词时,开启前缀缓存后,同等并发下的显存峰值能降到原来的30%-40%,首token延迟也明显降低,因为prefill阶段不用重新计算公共部分。如果你的业务里有这种"高频公共前缀"特征,这几乎是必做的优化。
需要注意,前缀缓存有一个"命中率"问题。只有前缀完全一致才能命中,有一点差别就失效。所以你要控制提示词的稳定性,尽量不要在系统提示前面拼接动态变化的用户ID、时间戳这类变量。把稳定的部分放在前面,动态的部分放在后面,前缀缓存命中率会高很多。
4.3 缓存一致性与失效:长上下文里的"缓存雪崩"
缓存有复用就有失效。KV缓存的失效分两种:一种是指标层面的,比如显存不够了要淘汰一些缓存条目;另一种是逻辑层面的,比如用户更新了上传的文档,那基于旧文档算出的KV缓存就必须作废。
大多数推理框架处理的是第一种,用LRU这类淘汰策略管理KV缓存池。但第二种失效往往被忽略。我自己踩过坑:给一个知识库做定时更新,更新之后用户提问时,系统还是优先命中了旧文档的KV前缀缓存,导致回答里出现已下架的商品信息。排查了很久才发现是前缀缓存没有做版本标志。
这里给一个务实的做法:给每个文档或者知识库文件计算一个内容指纹,内容变化时指纹变化,然后把这个指纹拼进缓存key里。这样内容更新后,旧缓存自然失效。另一方面,缓存淘汰别只依赖框架默认的LRU,最好对业务做分层:核心文档的KV缓存用独立的显存池,不让临时请求把它们挤掉。
5. 跨页推理:用操作系统的虚拟内存思路管理KV缓存
如果说Attention优化是让计算更快,提示压缩是让信息更少,KV缓存管理是让存储更高效,那么跨页推理解决的是最底层的一个问题:KV缓存这块内存怎么组织、怎么分配、怎么换入换出。这一块借鉴的是操作系统里虚拟内存的分页思想,代表是vLLM的PagedAttention方案。我觉得这是近两年推理系统里最有"工程美学"的设计之一。
5.1 为什么KV缓存会碎片化
在原生实现里,KV缓存通常是一整块连续显存,大小按请求的最大可能长度预分配。比如一个请求最大允许生成1024个token,系统就一次性分配好1024个token的KV显存。问题是,很多请求根本生成不了那么长,导致大部分显存被闲置;而且不同请求的显存块大小不一,随着请求的完成和释放,显存里会出现大量空洞,新的请求即使总空闲空间足够,也因为找不到连续的块而申请失败。这就是碎片化。
我用过一个早期推理框架,显存明明还有40%,但新请求进来报"CUDA out of memory"。用nvidia-smi看显存,空闲空间都是细碎的小块。这种情况在长上下文场景尤其严重,因为KV缓存体积大,更难以找到连续的地址空间。
5.2 PagedAttention:把KV Cache当成操作系统的页
PagedAttention的核心想法是:不要为每个请求分配一整块连续的KV缓存,而是把显存划分成固定大小的"页"(block),每个请求的KV被分页存储,逻辑上连续,物理上可以散落在任意位置。用一个页表(block table)来记录每个请求的逻辑页与物理页的映射。这几乎就是把操作系统的虚拟内存机制搬到了GPU上。
这样一来,之前的碎片化问题被解决了,因为物理块的大小是统一的,不会产生难以利用的小空洞。同时,显存利用率大幅提高,因为不需要按最大长度预分配,而是按实际使用量动态分配页面。更重要的是,共享前缀的请求可以映射到同一批物理页,多个请求指向同一个物理页的不同偏移,这就把第4节说的前缀缓存从"应用层优化"变成了"系统层原生支持"。
这套设计让推理服务的吞吐率有了非常显著的提升。我记得vLLM刚出来时放出的评测数据,在相同的硬件上,吞吐相比当时常用的推理框架能提升两三倍,靠的并不是什么魔法,就是背后的分页KV缓存管理。
5.3 物理地址分散后的注意力计算与跨页调度
分页节省了内存,但也带来一个新问题:计算Attention时,一个token的Query要去和所有历史token的Key做点积,而历史KV现在散落在不同的物理页里,你不能直接拿一整块连续内存去做矩阵乘,必须先把散落的页映射到连续的逻辑视图,或者用特殊的kernel让计算时按页索引去取数据。
我在调优时遇到过性能退化的情况:开了PagedAttention之后,显存占用降低了,但吞吐并没有翻倍,反而在某几个并发档位下变慢了。后来分析发现是block size选得太小,页表查询和kernel内索引开销占比偏高。block size调大之后,性能才恢复正常。这个教训是:分页大小是一个关键参数,太小了索引开销大,太大了碎片化又会回来。通常16或32个token一页是比较合理的起点,具体要结合显存带宽和你模型的隐藏层大小去测。
另外,当KV缓存整体超过显存容量时,就需要把一部分页换出到CPU内存,这就是"跨页"的进一步延伸。推理框架可以像操作系统一样,把冷页换出,热点页留在GPU。但这会引入一次PCIe数据传输,如果换页策略不聪明,会严重影响推理延迟。我的经验是,换出要按"整页"来做,优先换掉最久没被访问的页,并且要对单次换页的数据量做限流,防止一次换出太多导致微秒级别的卡顿。
5.4 除了vLLM,跨页思想还出现在哪里
现在很多人提到跨页推理就想到vLLM,但分页KV缓存的思想已经被广泛吸收到各类推理框架里。比如SGLang的RadixAttention处理的是前缀树的缓存共享,它在逻辑层面把公共前缀组织成树结构,让不同请求共享中间节点;TensorRT-LLM也有类似的分页KV缓存能力。如果你的部署框架不支持分页KV缓存,那么在高并发长上下文场景下,显存利用率基本是不达标的。
还有一个方向是"层内跨页":把不同层的KV缓存分配到不同的物理页上。因为这个模型有32层,如果每一层的KV都恰好在同一物理块里,整体的换入换出比较低效,层和层之间的访问频率不一样,冷热差异也更明显。工业实践里通常会把"层"这个维度也考虑进页表的组织方式中,让换页粒度更精细。
6. 长上下文工程落地:实测中值得记住的选型与调参思路
前面几节把Attention、压缩、缓存、跨页推理每个单点的原理和方案都过了一遍。但实际落地时,它们从来不是孤立选择的。你这套系统里,到底该用哪几种策略组合,取决于你的业务特点。这一节我给几个判断框架,附带一些我做实测时记录下来的数据和经验。
6.1 先分清你的瓶颈是显存、算力还是带宽
拿到一个长上下文需求,先不要急着上各种优化手段。第一步是搞清楚瓶颈在哪。我见过有人一上来就开量化、开SageAttention、开PagedAttention,结果显存降下来了但延迟反而变差。因为那个场景的瓶颈本来就是计算吞吐,不是显存容量。
判断方法很简单:开一个监控,分别记录显存占用、GPU利用率和Attention kernel耗时占比。如果显存接近上限,瓶颈在存储,优先考虑KV缓存压缩、分页、共享前缀;如果显存还有余量但GPU利用率很低,瓶颈可能在数据加载或者算子效率,这时优化FlashAttention、调大batch才是重点;如果整体GPU利用率很高但延迟还是不够,那是算力上限,要么换更大算力的卡,要么从压缩信息量入手。
6.2 上下文长不等于全量存:结构化去重和信息分层
第二个经验是:在实际业务里,"长上下文"往往不是真的需要模型同时看到所有原始token。很多信息是可以分层处理的。比如一篇长文档,你可以先让一个模型生成文档的结构化摘要(章节级别),再根据问题定位到具体章节,只把相关章节的原文送进长上下文模型。这比直接把整篇文档塞给模型要稳健得多,而且显存开销小一个量级。
这个思路和提示压缩有重合,但它更像一种"检索增强+长上下文"的混合架构。我的实测结论是:对于35K token以上的文档问答,这种"先摘要定位、再局部精读"的方法,在回答质量上并不输给全量灌入,但显存和延迟表现好太多。当然,如果任务是"跨章节综合推理",比如要对比文档开头和结尾的数据,局部精读就不够了,这种情况还是得让模型看到全量上下文。
6.3 不同压缩/缓存方案的组合优先级
在资源有限的情况下,我建议按下面的优先级来排优化手段:
- 确认FlashAttention等算子级优化已开启,这是基础收益。
- 做前缀共享缓存,尤其是系统提示词和公共文档固定的场景,收益立竿见影。
- 设置KV缓存上限和并发配额,保证服务稳定性,避免某个请求拖垮全局。
- 开启PagedAttention这类分页KV缓存机制,解决显存碎片化问题,提升吞吐。
- 如果还有瓶颈,再考虑提示压缩、状态记忆压缩、或做更长的上下文裁剪策略。
这个顺序是我拍脑袋反推出来的吗?不是。它基本是按照"风险从小到大、收益从确定到不确定"来排的。前几步基本上不损失模型能力,只优化工程效率;后面几步比如提示压缩,虽然收益大,但可能影响回答质量,需要做评测把关。团队里如果没有人能承担质量回归的风险,我宁可保守一点,只做前四步。
6.4 一个真实项目的MIX方案
最后分享一个我在长文档问答上的典型配置,可以作为参考起点。模型是7B,上下文窗口16K,但业务里经常出现用户上传35K字的文档。
- 方案:先用一个快速摘要模型把文档分段摘要,摘要后的内容控制在8K以内,再带着用户的原始问题进行全量推理。
- 缓存:把近期高频的几篇文档的KV前缀放到独立的显存池,长期不用的文档KV直接丢弃不缓存,每次现算现用。
- 推理框架:使用支持PagedAttention的方案,block size选32。
- 结果:显存峰值从接近满载降到60%左右,首token延迟的中位数从约2.4秒降到1.1秒,答案质量在测试集上没有显著下降。
这个组合不是最优解,但它是稳健、不易翻车的方案。每个团队的业务不一样,你可以参考这个思路去调整"哪一步做什么"。只是记住一点:不要追求把上下文做到模型的上限,"够用且稳定"在工程上永远比"最长但偶尔会崩"更有价值。
6.5 长上下文监控的必备指标
在优化过程中,我强烈建议把以下指标全部纳入监控:
- KV缓存总占用量:直接反映存储压力,能预警OOM风险。
- 每请求的平均KV缓存大小:看出你的长上下文是否真的被用起来了,还是说大部分都是浪费。
- 前缀缓存命中率:这个指标低,说明你的共享缓存设计有问题。
- PagedAttention页数/页利用率:页利用率太低,说明碎片化还是存在,换页操作频繁。
- 换入换出次数和耗时:如果跨页调度频繁,你的推理延迟会很飘,有必要优化冷热数据分布。
这些指标在不同框架里名字可能不一样,但对应的概念是一致的。把监控搭起来之后再调优,你会少走很多弯路。我以前靠直觉猜瓶颈,浪费了大量时间,后来老老实实看指标,很多问题一眼就定位了。
长上下文这个方向还在快速演进,今天看起来最优的方案,过半年可能就有新的替代。但底层的那几个核心问题——Attention的复杂度、KV缓存的存储、信息的冗余、内存的组织——始终是绕不开的。把这四条线的原理吃透,无论后面出什么新的框架或算子,你都能很快判断它到底解决了哪个环节的问题,值不值得引入。这也是我写这篇长文最想传递的东西:不要追着名词跑,要盯着账本看。