Beancount复式记账(四):项目管理

Beancount系列文章的前三篇已经基本覆盖了常用的复式记账方法。记账本身是一门经验的学问,不仅包括账本身怎么记,还包括了「账本」怎么整理。这篇文章不涉及复杂的会计学概念,只是从更加微观的角度来讲一讲我实际记账过程中是怎么组织的。

版本管理

文本记账的最大优势就是它便于使用版本管理系统,像管理代码一样管理账本。最常见的代码版本管理工具就是git了,所以我推荐使用git管理Beancount的账本文件。使用git的好处是提升账本的可维护性,尤其是能够防止不小心改错、误删这样的动作,这在重构的过程中极其重要。

git如何使用我不再赘述,对于会写代码的人来说属于基本技能。即使不熟悉,网上的资料和教程也是汗牛充栋了,更有Sourcetree这样的可视化工具。

惟一需要注意的是,你需要保管好你的git仓库,尤其是要避免盲目上传到Github之类的网站。账本信息属于非常敏感的个人隐私,因为其中可以透露出的信息非常丰富,甚至超过了日记能包含的。如果要上传git仓库备份,至少要使用支持私人仓库的服务,或者自己搭建git服务器(支持SSH即可)。最好在上传之前对整个仓库加密,譬如使用git-crypt

标签

标签是一个讲交易组织归类的方法,是开支类别之外的另一个维度。每一个交易都能加上一个或多个标签:

2019-06-01 * "奥地利航空" "东京-维也纳" #2019-07-Europe-Trip
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

标签的作用是方便查询,在fava和bean-query中都可以按照标签来过滤。

为了避免重复,Beancount还提供了标签堆栈语法:

pushtag #2019-07-Europe-Trip

2019-06-01 * "奥地利航空" "东京-维也纳"
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

2019-06-01 * "奥地利航空" "维也纳-莫斯科"
  Expenses:Transport:Airline 100 USD
  Liabilities:US:CreditCard:Citi

poptag #2019-07-Europe-Trip

除了标签,Beancount还提供了一个类似的语法^,叫做链接(Link),本质上和标签是一样的作用,但是被建议用作将财务上关联的交易组织在一起的方法。常见的使用场合是有时间跨度的一笔交易,例如汇款和收款,短期的债务,按次记录但是按月征收的某些银行手续费,或者仅仅是两个目的一致的交易。

2016-05-03 * "Chase" "取现" ^2016-05-overdraft
  Assets:Bank:US:Chase:Checking -75 USD
  Assets:Cash:USD 50 USD
  Expenses:Finance:BankFee:Overdraft 25 USD

2016-05-05 * "Chase" "还清欠款" ^2016-05-overdraft
  Assets:Bank:US:Chase:Checking 25 USD
  Assets:Bank:US:Chase:Saving

多文件组织

到此为止我一直假设所有的Beancount记录都是在单一文件中的,这个文件会随着账目的增多越来越膨胀,直到用编辑器维护不便。使用单一文件就像把一个巨大的程序写到一个源文件中一样,阅读和修改都很困难。所以Beancount提供了include文件包含语法,用法和多数编程语言一样。

include后面紧跟着要引入的文件名,路径是相对于当前文件的。

; main.beancount
include "accounts.beancount"
include "categories.beancount"
include "books/books.beancount"

; books/books.beancount
include "2016.beancount"
include "2017.beancount"
include "2018.beancount"
include "2019.beancount"

利用这个简单的语法,一个巨大的账本就可以分成若干个较小的账本组合起来了。

要注意,到目前(Beancount 2.2.1)为止,标签堆栈和文件包含是不能组合使用的,也就是说标签堆栈内的include的文件不会自动加上标签。我在Beancount的问题列表提出了这个问题,作者的答复是也许以后会实现。

账本划分

接下来我终于要讲到我的经验之谈了。尽管有文件包含语法,但每条记录到底怎么划分还是一个见仁见智的话题。我使用了三种分割方法,分别是按日期划分、按类别划分和按账户划分。这三种划分方式都有道理,各有优劣。总而言之,划分的目的是减少错误的可能,降低维护成本,节约注意力资源。下面我详细说来。

按日期划分

按日期划分账本是最直接的分割方法,我们可以按年份或者月份创建文件(譬如2019.beancount),每个文件内只包含这段时间内的记账。一般来说除非是修正错误或者重构,旧的账不会再修改,使用当前的账本来记账,把过去的账分割储存是一种有效节约注意力资源的方法。

除了按照日历时间划分,还可以结合标签,把某些事件提取出来,最常见的是旅行。下面这个例子是我把2019年7月关于欧洲旅行的账目全部放到2019-07-Europe-Trip.beancount中,并且结合标签堆栈,把整个文件中的账目标记上#2019-07-Europe-Trip标签。

; 2019-07-Europe-Trip.beancount
pushtag #2019-07-Europe-Trip

2019-06-01 * "奥地利航空" "东京-维也纳"
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

...
poptag #2019-07-Europe-Trip

使用单独的文件来记录某个时间段的某类事件相关的开销的好处是,一旦这个事件结束,这个文件就可以进入封存状态了。此外,哪怕是不使用任何可视化工具(如fava),这个文件的可读性也非常高,甚至可以当作旅行日记了。

资产负债表

按类别划分

按类别划分指的是把统一类的重复记录整理到一起,便于根据时间纵向比较。譬如说每月的工资单、房租、水电费、定期投资、以及其他自动扣费的服务。

; Tokyo-Electric-Power.beancount

2018-04-25 * "东京电力" "电费"
  Expenses:Utility 4621 JPY
  Liabilities:JP:PrestiaVisa

2018-05-25 * "东京电力" "电费"
  Expenses:Utility 4956 JPY
  Liabilities:JP:PrestiaVisa

2018-06-25 * "东京电力" "电费"
  Expenses:Utility 6648 JPY
  Liabilities:JP:PrestiaVisa

2018-07-25 * "东京电力" "电费"
  Expenses:Utility 9394 JPY
  Liabilities:JP:PrestiaVisa

以上的例子是每月25日扣费的东京电力账目。这样记录的好处是每个月的开销一目了然,还可以观察不同月份的开销变化。如果可以从扣费的服务导出账单,就可以把单个来源放在一个文件。如果是手动记录,这种组织方式也可以最大程度上减少漏记的可能性。

按账户划分

我使用的第三种分割方法是按账户划分。按账户划分和按类别划分差不多是相互对称的两种划分方法,因为这种方法是以账目相关的账户为依据,分割出了单独的账本。按账户划分的最初目的是方便自动导入脚本,因为许多银行、信用卡都可以提供月对账单,非常适合自动转换为Beancount格式,减少人工。

但是完全按照账户的缺点也显而易见,那就是尤其和「按类别划分」冲突,所以我的实际手段是尽可能不使用这个划分方法。什么时候会使用呢?我的原则是和账户的关联性十分强的消费,包括银行利息、银行手续费、信用卡还款、返现返点、开户奖励、年费、账户间转账。

; AmexSPG.beancount
2019-02-01 * "American Express" "SPG年费"
  Expenses:Finance:CreditCardFee 33480 JPY
  Liabilities:JP:AmexSPG

2019-02-01 * "American Express" "Amex SPG开卡奖励"
  Assets:Points:US:Marriott 30000 P_MRT
  Income:OpeningBonus

2019-02-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-02.pdf"

2019-03-11 * "American Express" "2019年2月账单还款"
  Assets:Bank:JP:SMBC -3254 JPY
  Liabilities:JP:AmexSPG

2019-03-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-03.pdf"

2019-04-10 * "American Express" "2019年3月账单还款"
  Assets:Bank:JP:SMBC -63935 JPY
  Liabilities:JP:AmexSPG

2019-04-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-04.pdf"

2019-05-10 * "American Express" "2019年4月账单还款"
  Assets:Bank:JP:SMBC -23718 JPY
  Liabilities:JP:AmexSPG

总结

我把我的原则总结成了以下规则,按顺序判断:

  • 如果该收支属于一个特别的事件,那么就将其加入这个事件对应的单独文件,例如#2019-07-Europe-Trip
  • 如果该收支是一个周期项目,但跟某个银行账户无关,则加入按类别划分的文件,例如水电费、房租、工资。
  • 如果该收支跟某个银行账户紧密相关,则加入该账户对应的文化,例如银行手续费、信用卡还款、返现。
  • 不属于以上情况,则加入按日期划分的文件。

以上就是我目前对账本管理的一些经验之谈,这些方法只是为了达成我前面提到的三个目的:减少错误的可能,降低维护成本,节约注意力资源。如果之后我发现了更好的划分方式,我会对账本重构,到时候再来更新这篇文章。

在以后的文章中(如果有的话),我还会介绍更多实际的记账方法,包括出差记账、更细致的资产记账,价格追踪,股票交易和多人记账。欢迎加入Beancount中文讨论:t.me/beancount_zh

以下链接是其他介绍:

Beancount复式记账(三):结余与资产

先前一篇文章里我介绍了Beancount的基本记账方法和一切常见的规范。这篇继续讲述Beancount的常用语法,主要内容是对账和资产折旧。

对账

Beancount的语法检查保证了每一笔交易的借记和贷记是平衡的,这已经可以避免许多会导致「账不平」的错误,但是对于数额本身的错误,或者某调账目漏记并没有办法。这就是为什么我们要定期对账。

资产负债表

所谓对账,就是看每个账户的结余(Balance)是否正确。每个账户的余额可以在资产负债表(Balance Sheet)页面中找到,可以查看样例资产负债表

资产负债表

fava资产负债表把资产列在左边,负债和权益列在右边。权益即净资产,是根据资产和负债计算出来的(除了Equity:Opening-Balances,之后会讲到)。

需要注意的是Beancount的债务和权益是负数,所以并不是资产 = 负债 + 权益,而是资产 + 负债 + 权益 = 0。我之前提过一次,这是Beancount使用正数来表示借记(Debit),负数表示贷记(Credit)的结果。在传统的复式记账中,数字的正负号并没有这样的意义,无论是借记还是贷记都是正数,所以绝对值资产 = abs(负债 + 权益)也许更好理解。

在资产负债表上点击任意账户,可以进入账户的明细界面。账户的明细界面列出了涉及该账户的每一笔交易,点开后可以看到具体的交易信息。每一行的最右侧是这一笔交易后的该账户结余,这个数字就是对账的关键。

账户明细

结余断言

假设已知账户的某日结余金额,只要在这个账户明细界面看一看对应的日期的最后一笔交易后结余是否正确就可以了。如果Beancount计算出的结余和已知的是一样的,那么基本上就可以确定账没有问题。

这个步骤看似容易,但是随着账目增多,对账的负担会很重,而且容易看错。更严重的问题是,如果因为某种原因要修改过去的账目,已经对好的账就不一定正确了。惟一保险的办法是每次修改了过去日期的交易后,把涉及到的账户未来的结余再全部重新对一遍。好在这个过程是可以自动化的,方法就是使用结余断言(Balance Assertion)。

结余断言就是在记账中加入已知事实,即某个日期开始的时候的某个账户结余。如果你对单元测试有了解,这个方法肯定不会陌生。结余断言的语法非常简单,如下例所示:

2017-08-20 balance Assets:US:BofA:Checking 2298.50 USD

惟一需要留意的地方是,结余断言是所声明日期开始的时候的余额,即当日的交易不算在内。

声明结余断言之后,Beancount会自动检查断言是否正确,如果不正确就会有错误出现。如果每个账户都有适当的结余断言,修改过去的交易就可以放心进行了。

文档链接

有了结余断言,接下来就是这个断言的事实从哪里来。最简单的方法当然是看看现在有多少结余,然后直接写上今天的日期。这种方法适合现金和其他一些不方便查询交易记录的账户。

除此之外,推荐使用银行月结单(Statement)上面的数字。许多国家的许多银行、信用卡都会定期发送交易明细,一般来说是每月。Beancount提供了管理这些文件的一个语法,例如:

2013-03-20 document Assets:US:BofA:Checking "path/to/statement.pdf"

在fava中这条记录也会被显示出来,并且提供可以点击的链接。这个路径是相对于这条记录所在的Beancount文件的目录,这对于多文件记账很重要(include语法,之后会讲到)。

根据个人经验还有一个重要的提醒,就是要看清楚月结单包含哪些交易,尤其是账单周期末的哪些。因为很多银行、信用卡的交易并不是即时结算的,特别是有跨国交易的时候。如果发现某些交易还没出账或者账单周期开始包含了上个周期的交易,一定要注意余额断言的数额。

结余调整

接下来说一说账对不上的情况。事实上,这是一种常态,人毕竟不是完美的,错记漏记实在是太正常了。一旦结余断言失败,当然是先看自己有没有记错什么,如果实在困难,或者金额差距不大自己没那么在乎,可以考虑用结余调整。

结余调整并不是非要用什么特别的语法不可,实际上只是一种规范。这个规范是使用Equity:Opening-Balances来表示初始结余。下面是一个例子:

2015-01-01 * "账户初始"
  Assets:US:BofA:Checking 3490.52 USD
  Equity:Opening-Balances -3490.52 USD

这个例子的意思是,在2015年1月1日给Assets:US:BofA:Checking增加3490.52美元结余。假设这个账户之前没有记录的话,那么它现在的结余就是3490.52美元,如果有就是在原来的基础上加上3490.52美元。这3490.52美元从哪里来呢?Beancount的规范是使用Equity:Opening-Balances

Equity:Opening-Balances是权益类别下面的账户,它是净资产的一部分。Beancount中权益是负数,所以数字减少代表了净资产的增加。这一部分资产的来源可以理解为表外资产,即来源不明确,在Beancount账本中没有更详细的记录。

Beancount还提供了一个更加简易的语法pad,结合了结余断言,功能是把结余调整到使得下个余额断言满足。用法如下所示:

2015-01-01 pad Assets:US:BofA:Checking Equity:Opening-Balances
2015-01-02 balance Assets:US:BofA:Checking 3490.52 USD

以上的效果是,无论Assets:US:BofA:Checking之前余额多少,在2015年1月2日开始之前都调整到3490.52 USD,差额从Equity:Opening-Balances来。

一般来说除非是调试错误或者导入数据过程中,否则我不建议使用pad这个语法,因为它会让结余断言丧失一定的准确性。使用pad的风险是,如果自动调整的数额过大,当修改了过去其他的账目导致需要调整的数额发生改变的时候,Beancount并不会出现任何警告和错误。我更倾向于把需要调整的金额明确写出来,这样一旦变化就会有错误提醒,避免更大的错误。

资产购入与折旧

生活中有一些交易我们需要考虑到底要记为花费,还是资产的购入。在光谱的两端一般没什么争议,譬如吃饭肯定是消费,买房无疑是购入资产。中间许多类别就不一定了,这取决于个人的偏好和目的。

拿买汽车作为例子,无论是新车还是旧车,许多人在开一段时间以后会选择卖掉。如果我们把买车记为消费,卖车记为收入,这本身并没有任何错误。问题是,对很多人来说汽车还是一笔不可忽略的资产,如果直接记为消费,没有对应的资产入账,那就意味着净资产突然大幅减值。几年后卖出,净资产又突然增加。

要解决这个问题,我们就要把汽车记为一项资产,下面是例子:

2015-01-01 * "丰田汽车" "买入Corolla"
  Assets:Car:ToyotaCorolla 20000 USD
  Assets:US:BofA:Checking -5000 USD
  Liabilities:Loan:Car -15000 USD

2019-01-01 * "二手车商" "卖出Corolla"
  Assets:Car:ToyotaCorolla -20000 USD
  Assets:US:BofA:Checking 10000 USD
  Expenses:CarUsage 10000 USD

上面这个例子是2015年1月1日贷款买入了价格为$20000的汽车,并计入资产Assets:Car:ToyotaCorolla。中间省略还贷款的过程,四年后2019年1月1日,把车卖给了二手商,获得$10000,剩下的$10000就是三年来用车的消费了。

这个方法对买卖交易之间的这段时间内净资产计算仍然不够准确,并没有完全解决净资产跳变。因为汽车的使用是三年来平均花费出去的,而不是最后卖的时候一下子花了$10000。要解决这个问题就要引入定期进行折旧(Depreciation)计算。一般的会计方法中把因为资产使用或者随着时间自然减值称为折旧,我记为Expenses:Depreciation:CarUsage

接下来需要决定的是折旧的周期,即把车的使用费按照多大的粒度来记录。这个完全因人而异,也因金额的大小而异。对于车我可以选择按年折旧,因为在许多国家(譬如美国),工作原因的资产折旧是可以按年抵税的。如果希望每个月的花销更加细致,那么按月折旧也是一个方案,只是需要多记录几笔而异。

2015-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2016-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2017-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2018-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

下一个问题是,每次折旧减值多少。这个问题就是会计上可以操作的空间了,因为实际的价格只有在出售的时候才知道。一般会计准则是预估一个折旧年限,然后以此为根据来折旧。譬如说,我们预期汽车的寿命是10年,即10年后该车的价值清零,这样我们可以按照每年10%的折旧率每年减记。最终当实际卖出的时候,我们再根据卖出价格做调整,或者差额记为其他类别。如果有特别的事件发生,还可以另外单独折旧。例如发生了车祸,车的估值大幅下降,可以在此时额外减值。

Beancount并没有自动折旧的功能,每一笔都是要自己写的。如果怕忘了,其实可以把未来日期的折旧也写上。另外还有第三方的插件beancount-interpolate可以尝试使用。

到此为止我差不多已经讲到了日常使用中的大部分语法。如果把这个系列文章看成一个编程语言教程,那么语法已经差不多讲完了。下一篇文章我会讲述「真是世界」里的Beancount使用方法——把Beancount账本看成一个项目,该如何管理。欢迎加入Beancount中文讨论:t.me/beancount_zh

Beancount复式记账(二):借贷记账法

上一篇文章介绍了为什么要复式记账,以及Beancount的基本特点。这篇言归正转,直接从实践开始。

账户类别

复式记账的最基本的特点就是以账户为核心,Beancount的系统整体上就是围绕账户来实现的。之前提到的会计恒等式中有资产、负债和权益三大部分,现在我们再增加两个类别,分别是收入和支出。Beancount系统中预定义了五个分类:

  • Assets 资产
  • Liabilities 负债
  • Equity 权益(净资产)
  • Expenses 支出
  • Income 收入

这五类是Beancount的约定,除此了Equity之下一些特殊的账户外,没有任何预先定义的账户。用户可以定义各种各样的账户,Beancount对账户的组织是树形的,譬如我分别有这些资产账户:

Assets:Cash:JPY
Assets:Cash:USD
Assets:Bank:CH:UBS
Assets:Bank:CN:BoC
Assets:Bank:US:Chase:Checking
Assets:Bank:US:Chase:Saving
Assets:Bank:JP:SMBC:JPY
Assets:Bank:JP:SMBC:USD
Assets:Broker:US:IB
Assets:Points:Airline:JAL
Assets:Points:Airline:United

Beancount对这些账户的组织形式如下图:

账户

接下来是声明账户的语法。Beancount要求每个使用的账户必须声明开户时间,格式是YYYY-mm-dd。之后是关键词open,表示在这个日期开户(或者开始记账)。接下来是账户名称,格式用:隔开的树形语法,最后是(可以省略的)账户的货币种类。货币种类不需要事先定义,也没有系统内部的定义,一般来说我们使用三字母的货币代码,但其实可以用任何的名字(惟一的限制是大写字母和下划线)。我在这个例子中使用了USDJPYCNYCHF四个货币,以及我自定义的P_JALP_UA表示不同航空公司的里程。

2019-01-01 open Assets:Cash:JPY JPY
2019-01-01 open Assets:Cash:USD USD
2019-01-01 open Assets:Bank:CH:UBS CHF
2019-01-01 open Assets:Bank:CN:BoC CNY
2019-01-01 open Assets:Bank:US:Chase:Checking USD
2019-01-01 open Assets:Bank:US:Chase:Saving USD
2019-01-01 open Assets:Bank:JP:SMBC:JPY JPY
2019-01-01 open Assets:Bank:JP:SMBC:USD USD
2019-01-01 open Assets:Broker:US:IB USD, JPY, CHF
2019-01-01 open Assets:Points:Airline:JAL P_JAL
2019-01-01 open Assets:Points:Airline:United P_UA

账户如何组织分类完全看个人需求和喜好,譬如我先分账户类型,再分国家,然后是金融机构名,最后是具体账户。分类的作用是可视化的时候,可以看某个非字节点下面所有账户的汇总。

接下来是负债类别的账户,最常见的就是信用卡,组织方式和使用方法和资产账户没有什么区别,譬如:

2019-01-01 open Liabilities:CreditCard:US:Discover USD
2019-01-01 open Liabilities:CreditCard:JP:Rakuten JPY

在开始记账之前,还差最后一步,就是收入和支出类别的定义。Beancount把收入支出的类别也想象成了一个账户,在语法上和资产、负债类账户没有区别。譬如下面例子:

2019-01-01 open Expenses:Clothing
2019-01-01 open Expenses:Food:Dinner
2019-01-01 open Expenses:Transport:Airline
2019-01-01 open Expenses:Transport:Railway
2019-01-01 open Income:Salary
2019-01-01 open Income:Rebate

基本借贷记账

有了以上定义的账户以后,我们终于可以开始实践记账了。复式记账又叫作「借贷记账」。之所以这么叫,是因为每一条记录都至少有一条借记(Debit)和一条贷记(Credit)。可以看下面这个例子:

2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD
  Liabilities:CreditCard:US:Discover -1000 USD

这个例子表示我在日本航空购买了纽约-东京的机票,消费1000美元(贷记),付款的信用卡Discover扣款1000美元(借记)。

Beancount基本的语法如下所示:

YYYY-mm-dd * ["payee"] "description"
  posting 1
  posting 2
  ...

第一行要有日期,接下来是*。收款者payee是可选的,如果*后面只有一个字符串,那就是省略了payee。从第二行开始,每一行开头空两个缩进,然后是账户名以及金额、货币。这里有一个要点:要保证所有条目的总和是0,否则就会出现Transaction does not balance: (xxx USD)这样的错误。这个要求很好理解,因为花出去的钱必须和账户上减少的钱一样,否则就是所谓的「账目不平」了。

Beancount语法的灵活性在于每个记账单元可以有任意多个条目(借记和贷记),只要保证它们的总和是0就可以。于是我们还可以这样记录:

2019-01-01 * "Walmart" "在超市买两件衣服和晚餐"
  Expenses:Clothing 20 USD
  Expenses:Clothing 10 USD
  Expenses:Food:Dinner 10 USD
  Liabilities:CreditCard:US:Discover -40 USD

更加复杂的例子可能是这样的:

2016-01-01 * "Google" "工资"
  Assets:Bank:US:Chase:Checking 500 USD
  Assets:Bank:US:Chase:Saving 1839.35 USD
  Assets:Pension:US:401k:PreTax 419.23 USD
  Assets:Pension:US:401k:PreTax 209.62 USD
  Expenses:Health:Insurance:Dental 3.14 USD
  Expenses:Finance:Insurance:TermLife 7.67 USD
  Expenses:Health:Insurance:Vision 0.98 USD
  Expenses:Tax:US:Federal 763.26 USD
  Expenses:Tax:US:Medicare 60.84 USD
  Expenses:Tax:US:SocialSecurity 260.15 USD
  Expenses:Tax:US:State:NY 212.59 USD
  Expenses:Tax:US:City:NYC 131.57 USD
  Expenses:Tax:US:State:NYDisability 1.2 USD
  Income:Salary:Regular -4192.31 USD
  Income:Allowance:TermLife -7.67 USD
  Income:Salary:401kMatch -209.62 USD

实际的记账中,一进一出的两个账户占了绝大多数。这个时候把正负的金额写两遍未免有点罗嗦了,所以Beancount还提供了金额插值的功能。简单说就是假设总和一定是0,在有N个账户的时候,只要求N-1个账户声明金额。于是最初的例子还可以写成:

2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD
  Liabilities:CreditCard:US:Discover

在我的实践中,我只在正好是两个账户的时候才使用这个功能,因为可以避免重复的数字。但有多个账户的时候,把每个金额都写出来有助于避免错误,和「防御性编程」的理念一样。

在上面的例子中,我们还可以看出来,所有Expenses类别的账户都是正数,所有Income类别的账户都是负数。这是Beancount及类似工具使用正数来表示借记(Debit),负数表示贷记(Credit)的结果。同理,通常Assets类是正数,Liabilities类是负数。本质上每个账户的数值只有绝对值有意义,正负号并没有实际含义。

货币转换

如果在去国外旅游,免不了要进行货币转换。事实上Beancount本身没有定义任何货币,这也意味着你可以定义任何货币(或商品)。所以哪怕不出国,只要是记录了非主要货币类资产,譬如投资品,代金券,航空公司里程,那么就需要货币转换了。

在使用多个货币之前,需要先定义「工作货币」。工作货币可以不止一个,例如:

option "operating_currency" "JPY"
option "operating_currency" "USD"

定义了工作货币以后,在fava界面中可以看到工作货币单独列出的栏目。

Beancount货币转换的语法有两种,一种是使用@记录单位货币的转换价格,例如:

2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD @ 110 JPY
  Liabilities:CreditCard:JP:Rakuten -110000 JPY

另一种方式我更常用,使用@@记录转换后的总额:

2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD @@ 110000 JPY
  Liabilities:CreditCard:JP:Rakuten -110000 JPY

货币转换不一定只在一个账户上。下面的这个例子是以2.5日圆每点的价格,买了10000日本航空里程,但是付款的信用卡是以美元计价的,所以两遍都可以转换为25000日圆来平衡。

2019-01-01 * "日本航空" "购买里程"
  Assets:Points:Airline:JAL 10000 P_JAL @ 2.5 JPY
  Liabilities:CreditCard:US:Discover -220.0 USD @@ 25000 JPY

借贷管理

复式记账的强大之处是每个账户都有状态,而且每个操作都是原子的,这对复杂的资金进出记录非常有帮助。

生活中一个常见的例子是朋友之间的借钱和相互垫付,就拿我最近遇到一个例子来说吧,我和X、Y三人一起出游,从东京附近的横须贺坐船到猿岛,费用是每人1300日圆的船票和200日圆的登岛费,其中船票可以用信用卡支付,而登岛费只能付现金。我们一共需要付4500日圆,但是正好谁都没有这么多现金,于是决定我用信用卡付三人的船票,X用现金付三人的登岛费,最后再结算。

2019-05-25 * "猿岛" "渡轮"
  Expenses:Transport:Ferry 1300 JPY ; 个人渡轮费用
  Assets:Receivables:X 1300 JPY ; 对X应收账款
  Assets:Receivables:Y 1300 JPY ; 对Y应收账款
  Liabilities:CreditCard:JP:Rakuten -3900 JPY

2019-05-25 * "猿岛" "登岛费"
  Expenses:Transport:Attraction 200 JPY ; 登岛费
  Liabilities:Payable:X -200 JPY ; 欠X的钱

第二天,三人结算完毕,X付给我现金,Y转账给我。

2019-05-26 * "猿岛" "费用结算"
  Assets:Receivables:X -1300 JPY ; X偿还债务
  Liabilities:Payable:X 200 JPY  ; 偿还对X的债务
  Assets:Cash:JPY 1100 JPY ; X实际付给我的钱到账
  Assets:Receivables:Y -1300 JPY ; Y偿还债务
  Assets:Bank:JP:SMBC:JPY 1300 JPY ; Y实际付给我的钱到账

最终可以看出来,我在Expenses:Transport:Ferry类别消费1300 JPY,在Expenses:Transport:Attraction类别消费200 JPY,信用卡扣款3900 JPY,收到了1100 JPY的现金,Assets:Bank:JP:SMBC:JPY收到了1300 JPY的转账。

以上这种记账方法可以让资金的流动一目了然,类别和金额也准确无误。记录对他人的债权(应收账款)和欠他人的债务(应付账款),我分别使用了Assets:ReceivablesLiabilities:Payable下面的账户。

应收账款和应付账款的另一个用途是区分付款和到货时间。一般来说交易是当场进行的,一个账户的借记和另一个账户的贷记同时发生,但是有些时候付款和到货并不是同时发生的,如果需要精确区分发生的时间的话,可以用这种方法把他们分成两笔记录。

下面这个例子是,用信用卡买日本航空里程,但是不知道为什么过了1个月才到账:

2019-01-01 * "日本航空" "购买里程"
  Assets:Receivables:JAL 10000 P_JAL @@ 25000 JPY
  Liabilities:CreditCard:US:Discover -220.0 USD @@ 25000 JPY

2019-02-01 * "日本航空" "里程到账"
  Assets:Points:Airline:JAL 10000 P_JAL
  Assets:Receivables:JAL

这篇文章到此为止,Beancount最基本的用法已经介绍完毕了。用这些简单的语法,我们已经可以满足大部分的记账需求了,但Beancount的强大之处远远不止于此。

下一篇我会继续介绍Beancount的更多用法,包括如何高效对账和资产折旧。同时欢迎加入Beancount中文讨论:t.me/beancount_zh

Beancount复式记账(一):为什么

我在在Google的这四年(三)这篇文章中提到了一个「秘密武器」Beancount以及「复式记账」的概念。我说过我要专门写一篇文章来分享我的经验,这篇文章就是我承诺的兑现了。

如何实现财务自由

这个章节标题有点吸引人眼球,像是成功学教程。但是我保证我下面说的内容都是严肃,而且有明确定义和方法的。

为什么要记账,我来用一句话总结就是为了提升对自我的认识。记账是个人理财的基础,更是通往财务自由的必经之路。财务自由这件事情固然是根本需要赚钱来实现,但并不是只有一夜暴富才能实现的。财务自由其实并不是一件虚无缥缈的事情,而是每个人都可以努力达到的一种状态,换句话说就是「退休」,只不是有人能在三十多岁退休,有人要六十岁,有人则要到八十岁。

财务自由的一般定义是由资产产生的收入不少于生活开销。如果不知道自己有多少开销,甚至不知道自己有多少资产、收入,即便是一夜暴富,财务自由是一件不可能的事情。

接下来我来解释为什么财务自由是可以实现的,而且应该是每个人的目标。首先需要强调的概念是资产(Assets)净资产(Net Assets)的区别,虽然我之后还会详细解释,但是这里需要记住,财务自由并非对个人净资产的要求,这也就是为什么不需要暴富就能实现。像所有投资都有期限一样,个人的寿命也是有期限的。每当我想到我总有一天会死,都会觉得有些忧伤,但这是无法改变的自然规律。只要在预期的寿命之内资产(而不是净资产)产生现金流能够满足生活所需的开销,这就是财务自由。我对资产产生现金流的定义较为宽泛,不仅包括利息、分红、租金、版税这类收入,还包括了直接通过资产折现的收入,即净资产减少。

要想达到财务自由,需要三点要求:对支出的预期、对资产和收入的了解、对寿命的期望。这三点都是说起来容易,做起来难的,但是记账可以帮助你很大程度上解决至少前两个问题。至于第三个问题,才是财务自由的根本困难所在,不过这个问题的解决方案已经超出了本文探讨的内容了。

为什么要复式记账

记账这件事是那种容易让人因为一时冲动开始,但是很快就放弃的事情。每个记过账人都有不同的原因和契机,但能坚持下来的则凤毛麟角。记账的好处是提升对自己的了解,解决那种不知道自己赚的钱都到哪里去了的问题。但记账的困难也显而易见,那就是麻烦,还容易遗漏、错误。一般来说普通人对记账的理解就是每笔消费都干什么了,所以就是每一笔消费都有一个金额和类别就可以了。这种记账方式一开始简单,但能带来的价值有限,只是开支记录而已,长期看来难以说服自己为了这些价值而忍受麻烦的记账过程。

相比普通的流水账,复式记账的核心理念是账户之间的进出关系,要求所有的记录全部入账,它可以保证账目的完整性和一致性。复式记账可以提供除了开支记录之外的损益表、资产负债表、现金流量表、试算平衡表等报表。复式记账还可以把投资和消费轻易区分,譬如购入电脑、手机,可以作为资产项目入账并定期折旧。同理对各种代金券、点数积分的购入一样要算入资产而不是消费。

虽然有各种各样的手机应用号称可以简化记账,它们把用户交互做得更加简单友善,有的还可以从银行账户、信用卡公司直接抓取数据,降低心理障碍,但与此同时它们却带来了另一个问题,就是数据所有权、安全性和持久性的疑虑。这些工具大多都是把数据存储在云端的,泄漏隐私的可能性不言而喻。更麻烦的是,它们几乎都是自己的专有格式,无法导出保存,或者哪怕可以导出也难以使用。这也意味着你不得不一直用一个产品,直到有一天倒闭或者服务关闭为止。这对我来说是不可接受的,因为在互联网时代能活过十年的产品非常稀少,无论是像Google这样随意关闭服务的大公司,还是随时有可能倒闭的小公司。复式记账的价值在于数据的完整性,我对其数据寿命的要求是二十年以上。

复式记账是一种划时代的发明,这个发明被认为是源于中世纪的地中海城邦(意大利或者埃及犹太人)。复式记账技术成为中世纪至大航海时代复杂贸易的支柱性工具,使得复杂的合约、信贷成为可能。后世会计学、金融学的许多概念都来自于复式记账,可以说体系化的资本主义是从复式记账的实践中诞生的。

如何复式记账

网上对于复式记账的文章也是汗牛充栋了,但各种晦涩难懂的名词可能会吓跑人。我不打算在这里介绍什么是复式记账,因为很难一句话说清楚,说得复杂了又没有意义。其实你并不需要懂很多才能开始复式记账,只需要一些最基本的概念就可以了。这个概念就是会计恒等式。

会计恒等式

作为会计学的核心,复式记账是一种实践中诞生的技术。除了最基本的会计恒等式之外,复式记账只有规范,没有对错。那么什么是会计恒等式呢?最基本的形式就是:

资产 = 负债 + 权益(净资产)

这个等式对于没有接触过会计学的人来说可能不太容易理解,但是记账实践过马上会明白的。理解的难点在于,会计学上的「资产(Assets)」和一般人对一个人富有程度的理解不太一样,因为资产是负债和权益的总和,负债(Liabilities)也是资产的一部分。

更容易理解的部分其实是权益(Equity),在个人理财的上下文中,又和净资产(Net Assets)是等价的,也就是经常说的「个人净值」。净资产需要由总资产排除负债,一个人到底是否富有,看的是净资产,而不是资产

举例说明,一个人首付20万美元,贷款80万美元,买了价格为100万美元的房产。假设这个人没有别的资产和负债,那么他的资产就是100万美元,负债是80万美元,而净资产是20万美元。在房价没有变化的情况下,买房对一个人的净资产没有什么影响(忽略手续费的话),而他的资产和负债都暴增,也就是所谓的「扩大资产负债表」。

如果你理解了会计恒等式,那么复式记账的一切知识障碍已经扫除了。

使用Beancount

再说一遍,复式记账在于实践,在开始实践之前学很多艰深的会计学概念没有任何意义。要开始实践,就要有工具上手了。在众多工具中我推荐使用Beancount,原因如下:

  1. Beancount是一个开源工具,用Python实现的,可以本地运行。
  2. 账本是一套基于文本的语法,方便存储和管理,个人拥有全部的数据,还可以使用Git管理。
  3. 账本的语法很规范,也具备灵活度,像编程语言一样可以嵌套引入,也有语法高亮和代码检查工具。
  4. 有完整的命令行工具链和可视化工具fava,还有基于SQL的查询和报表生成。
  5. 没有预先定义的类别、货币等现实世界概念,可以轻松实现多币种记账,包括各种点数、虚拟货币。

除了Beancount,Plain Text Accounting网站还列出了其他的开源工具比较,类似的具有竞争力的开源工具还有Ledger和hledger,其中Ledger是这类工具的开创者。无论使用里面介绍的哪个工具,其基本理念都差不多,即记录账户之间的资金流动。最重要的是,你是账本的所有者,如果不喜欢一个工具了,可以轻易转换到另一个工具。虽然它们语法有些许区别,但写一个脚本来转换不难。

使用Beancount之前首先需要Python 3运行环境。Beancount可以轻易从PyPI获得,使用以下命令:

pip install beancount fava

上面的命令其中beancount是核心包,包括了命令行工具,fava是网页可视化工具。这里是一个示例账本:链接。示例文件可以在Beancount的Bitbucket上下载

下载之后可以在命令行中运行:

fava example.beancount

即可看到:

Running Fava on http://localhost:5000

Beancount

至此为止Beancount环境就已经设置好了。

这篇文章到此为止,下一篇我会介绍Beancount具体的使用方法和操作经验。同时欢迎加入Beancount中文讨论:t.me/beancount_zh

在Google的这四年(五)

十六、熟悉而又陌生的国度

2018年2月,我正式登陆日本,在这个熟悉而又陌生的国度开始新的生活。

虽然之前来过日本旅游两次,但是理解都浮于表面,只是惊叹于日本街道的干净和优良的秩序。哪怕现在我也很难说我对日本有多么深刻的认识,只是浸淫在这样的文化中,体验更加真实。日本是我生活的第五个国家(前四个是中国、英国、瑞士、美国),也是第一个中国以外的东方国家。

日本是一个熟悉而又陌生的国度。熟悉之处在于日本深受中国古典文化的影响,近代至今则反过来向中国输出了大量文化。陌生之处在于,相比汉字文化圈的朝鲜半岛和越南,日本对中国文化的吸收总是有分寸。这种感觉日本文化中隐隐约约有大唐遗风,但又绝对不是另一个「小中华」。日本文化的复杂性就像日文汉字和假名混合书写一样,一个完全不懂日文的中国人总是能看懂一部分,而其关键却藏在由假名构成的语法细节中。

除去中国文化的倒影之外,日本还是东亚第一个全盘西化的国家。所谓「东方国家」只是地理上的定义,日本在精神上早在一百年前就「脱亚入欧」了。

百年前东京鸟瞰

上面这幅绘画我还以为是欧洲某个城市的鸟瞰图,但这竟然是一百年前的东京市中心!尽管中国也有上海、天津租界,但西方建筑的规模和宏伟程度都远逊东京,更别说东京根本不是殖民地租界了。而这些建筑下落很遗憾,在太平洋战争期间美军密集的大轰炸之下已经荡然无存。

十七、初上陆

跟以前几次不一样,来到日本的第一个挑战就是语言障碍。之前在英国和美国因为是英语国家所以几乎没有什么交流问题,即使有也在短时间内就克服了。瑞士虽然讲瑞士德语,但几乎人人都会英语,再加上苏黎世三成以上的外国人口(Google苏黎世更是绝大部分),语言也一直不是问题。然而英语在日本的通行非常有限,尽管这几年旅游业有所改善,但生活中完全不会还是不行的。Google东京的绝大部分人还是讲英语的,所以工作并没有问题。出于希望深入了解这个国家的动机,我日语还是非学不可的。

我学习日语也算差不多完全从零开始,哪怕我提前认全了假名,而且研究过中古汉语(韵典网),对日语汉字音读的规律在某种程度上是掌握的,但也仅限于此了。一年后的今天,我的日语听说能力还是非常有限。问题很明确是在于日语的输入还是不够多,毕竟工作讲英语,我又不看动漫、日剧、偶像团体,我对日本文化的吸收大多是阅读日文。这里有几个美国人同事,他们出于对日本文化(ACG)的热爱来日本生活。对于他们来说,日语听说完全不是问题,难点只是在汉字而已。这也是为什么喜欢日本文化的中国人可以很快学会日语,因为既有直接输入,又天生对汉字难关免疫。

以我现在的能力,生活中和人交谈是非常尴尬的局面。譬如说,在买东西、吃饭等场合,我一般都会被默认为日本人讲日语,直到我开口暴露自己身份为止。我开口讲日语后,很快对方会说出一些我听不懂的句子,只有少数人具备简化语法和用词的能力调整到简单的日语,另一些人会切换到非常蹩脚的英语,剩下的人则会直接结束交谈。只有随著日语能力的进步,尽量减少我听不懂的部分,问题才可以解决。

与之相反,我的美国朋友抱怨另一种局面,即许多日本人看见他们的西方脸孔就直接讲英语,哪怕他日语不错,而对方英语很差,对方也坚持讲英语。如果以欧美的社会文化背景来揣度,这种现象容易被解读为日本人排外或者「种族主义」。但我认为这可能是出于日本服务业接人待客的一种规范,叫做「御持て成し(Omotenasi)」,大致说就是全心全意为客人考虑。只是由于文化背景的不同,这种为他人考虑的结果可能事与愿违,进而产生误解。

十八、法律与秩序

在2016年美国总统大选的一场电视辩论上,主持人提到了如何解决美国大城市如芝加哥的贫困和暴乱问题。特朗普的回答简单明了,就是实现法律和秩序(Law and order)。他的这个说辞被反对者批评为暴力镇压,忽视背后真正的问题云云,一直到今天还有人在争论。且不说「法律与秩序」是否真的被特朗普带回到了芝加哥,日本真的是一块「法律与秩序」之地。

迄今为止日本给我最大的感受就是对秩序的崇拜。在日本,不仅仅是法律,道德和未明言的社会规范都被奉为圭臬。有些人把这说成是日本的阴暗面,乃至是日本社会压抑的原因。我的观点相反,这正是日本社会最不可多得的优点。日本法律的执行非常严格,几乎可以算是说到做到,按纸面最保守的方式解读,而不像美国法律有诸多辗转腾挪的余地。

没有人否认日本是一个秩序井然的社会,这体现在方方面面:包括一尘不染的大街小巷,永远准时的公共交通,严守纪律的学生,各行各业一丝不苟的职员。这种对秩序的遵守大大降低了社会的摩擦,使得人与人之间相处融洽又保持必要的距离。严格的秩序感大幅提升了社会运转的效率,使得现实与预期不会有过大的出入。举例说明,人人都准时使得只有日本可以做到国内航班在起飞前15分钟还能通过安检,从容地走到登机口。

十九、同质与多元

在日本社会秩序的背后,支撑这些规则的是整个社会的高度同质性。同质的社会里每个人的差异不大,有相似的价值观和道德感,因此人和人更容易相互理解,纸面上未书写的社会规范则更有约束力;人和人也有更高的相互信任感,信任则会降低社会成本,给全社会带来更高的效率。

支持多元文化主义的人还总是提到多元文化会促进创新,但这个结论并没有实际的证据支持。先不说文化冲突带来的社会成本,多种不同的文化混合在一起是否更能更创新,都是个值得商榷的问题。

与高度同质的日本相反的另一端是五彩缤纷的印度。这块神奇的次大陆上有数百种不同的语言、宗教和族群。这些不同文化背景的各色人在印度联邦制度下有限度地和平共处,内部秩序非常薄弱。族群复杂的印度难以有内生的秩序,历史上一直需要外来统治阶级的不断入侵来建立秩序,从雅利安人到大英帝国殖民历经数千年。这也就是为什么印度人总是说,如果当年没有英国,今天的印度不可能是一个统一的国家。凡是去过印度的人都会对那里无处不在的混乱留下深刻的印象,这是其多元文化带来的后果。

多彩的印度

日本从来不会为了多元化而多元化, 哪怕是如今逐渐放宽移民政策,也要求移民融入日本。这为崇尚多元文化主义的英美国家提供了一个色彩鲜明的另类样本,在某些方面与瑞士不谋而合。

同质的日本

下一篇:在Google的这四年(六)(待续)

在Google的这四年(四)

十二、现金流压力

从瑞士到美国以后,工资大减近四成。虽然这是预期之中的事情,毕竟瑞士的基本工资真的很高,而税又很低。但真正到美国看到自己收入大幅缩水以后,还是有些难以接受。刚刚搬过来的时候还有各种事情纷至沓来,让我一度情绪低落。不过几个月后我终于适应了新的环境,同时因为升职和调薪我的收入超过了之前在瑞士的收入。我在上一篇在Google的这四年(三)里面提到美国的生活给我带来的一大收获就是对资本、投资、现金流的理解,不得不说其实也是因为一开始的资金压力给我带来的。

我的资金压力有很大一部分原因其实是投资过于贪婪带来的。我是年中到美国的,但又想把一年的401k(税前和税后)、IRA、HSA这些账户全部交满,结果就是每次工资到手只有几百美元。虽然有了复式记账以后从账面上看我的净资产是在增加的,但现金流的确非常可怜,困难到信用卡难以偿还的地步,我可谓是陷入了「流动性危机」。当然我还是不会信用卡最低还款缴纳巨额利息的,因为我通过学习了解到了一个强大的金融工具:「保证金借贷(Margin loan)」。保证金借贷其实就是把自己的金融资产作为抵押,获得一笔贷款,这笔贷款的利率通常非常低,甚至低于政府隐性担保的房贷。我印象中在2016年我付出的年化利率在2.5%左右。这个方法对我来说非常实用,因为我有股票、基金这样高流动性的产品,虽然迫不得已我可以变现,但有了保证金借贷以后我并不需要变现了。在美国如果有账面盈利变现是要缴资本利得税的。保证金借贷本质上和提高投资杠杆是一样的,只要我借得不太多(杠杆不太高),市场波动风险就不会影响到我。

十三、搜索基础架构

我继续参与了Google搜索的项目,但是从前端转到了后端。所谓Google搜索的前端,简而言之指的是用户查询时在线即时计算的部分。这包括关键字的语意理解、信息检索、排名算法等等。后端则是离线的部分,指的是网址发现、页面抓取、索引建立等这些不在用户查询时即时计算的工作。这个划分非常粗略,也不准确,Google的真实搜索系统要复杂得多。

在Google,大规模的离线数据处理系统也可以做到低延时,我做的恰恰是其中最实时的部分——Google搜索的即时索引系统。这个系统负责为新闻、Twitter这种高频率、低延时的内容建立索引。相比之前在瑞士的时候参与的语意理解项目,这个工作偏向于系统架构的设计。Google大规模数据处理的引擎很多,绝大多数都是为海量数据高吞吐量设计的,而这个系统则对实时性的要求很高,工作内容极具挑战。因为高实时性的要求,而且对Google搜索结果影响十分关键,这个系统有专门的运维工程师(Site Reliability Engineer)团队。此外我也不得不身兼一部分运维工程师的工作,某些时候要随传随到(Oncall)。作为开发工程师我的工作的重要部分就是让这个系统尽量稳定,这样也可以减轻自己的工作量。

即时索引

Google搜索对即时新闻的支持早在十多年前就开始有了。这是一个历史悠久的系统,代码经过多代演进,一环扣一环。它的学习曲线要比之前的项目(语义理解)陡峭得多,光是把系统的每个部分在干什么搞明白我就花了差不多大半年。

在这个项目的两年里,我的工作可以分两类,一类是维护现有的系统和在现有系统上开发新功能,另一类是开发下一代的系统取代现有系统。Google的许多重要项目每几年就要有一次更新换代。这样的项目还是很吸引人的,因为一旦成功不仅会有个人的成就感,还有更大的升职机会,无论是对于普通工程师还是项目的管理层。

十四、项目的风险

新项目是有风险的,因为成熟的项目通常都有巨大的技术负债,而且又十分重要,新的系统必须在各个方面都要比原来更好才能说服决策层下决心取代旧的系统。这其实对决策层来说也是一个长远利益和短期利益平衡的问题。Google搜索的任何大改动都有可能会给这个市值几千亿美元的公司带来重大风险,对短期来说可能又好处有限。而长期来看项目都是有生命周期的,随着「老化」会让开发越来越困难,陷入停滞,所以必须再适当的时候引入新的系统。新项目失败的风险总是存在的,可能来自技术难关——新的系统并没有比旧的更好;可能来自利益冲突——新的系统使某些团队的利益遭到损害;可能来自资源的投入——决策层可能会在某个时候改变人选或者改变注意,撤回对项目的支持。这些原因都会导致项目的流产,但对于基层的工程师来说基本上是不可控的。

技术原因的失败并不罕见,因为Google前人开发的系统已经在各个方面都很优异了,可能只是使用的技术栈比较旧而已。新的技术栈并不一定就比旧的更好。要证明新的系统比旧的在某关键方面要好很多,而且其他各个方面也都不能差,是一件很难的事情。如果不可能,就要退而求其次,证明新的系统「利大于弊」。这就更难了,什么是利什么是弊,对谁来说是利还是弊,都是需要考虑的问题。

好在目前看了这个机制还是在运转的。在升职及个人成就感的激励之下,Google的各种系统一直在不断迭代更新,始终引领技术发展,或者至少保持在前沿。这样的激励同时有一个副作用,即很难有十分稳定的系统。Google内外都有很多批评的声音,声讨Google「随意」关停项目,令用户失望。系统的不断更新甚至关停对于公司内部的下游使用者来说也是个痛苦的难题。有人这么总结「Google的系统要么是没有文档(快速开发中),要么是已经废弃了」。历史悠久的产品面临的一个重大挑战是它们依赖的子系统不时就会有通知说即将关闭,必须要在某个截止日期前迁移到新的系统。这样的通知意味着凭空出现的额外工作,而完成这样的工作吃力不讨好,没法说明自己工作的影响力。反过来说,上游系统开发者必须考虑新系统给下游带来的迁移成本。如果这个成本过高,又无法提供足够的技术支持,必然会有来自下游团队的激烈反对。这种情况就可能会带来所谓的「利益冲突」。

我在这个项目的两年可以说是很幸运的。因为一方面我的工作让Google搜索成功应对了AMP的爆发式增长,另一方面我们还成功发布了新系统,完整取代了运行十多年的老古董。在这个项目中,我除了积累了个人经验,还锻炼了重要的协作能力。如前文所述新项目会带来团队之间的利益冲突,这个项目也不例外。慢慢地我发现工程师的协作能力的很大程度上决定了一个人的高度,这是一个老生常谈的问题,但只有自己体会过才能理解为什么。个人技术能力呢?那当然更重要了,否则一切都是浮云。

十五、路径依赖

我大概是在2017年10月决定离开美国的,这回目的地是日本。这个决定可以说比离开瑞士更疯狂,更难被人理解。其实我自己也更难下决心,因为我离开那个团队可能并非当下职业发展的最佳打算。与此同时我还要再次面临收入的削减,会比上次从瑞士到美国幅度更大,即便是再次晋升也无法抹平的差距。最终,出于对不同的文化探索的热情以及一点冒险的基因,我下定了决心。

我是这么说服自己的,我毕竟当时才毕业工作三年而已,未来有无限可能,这些可能性要趁早尝试。许多顺风顺水的人在某个时候都会陷入路径依赖——尽管每个局部选择可能都是最优的,但最终归于普通。这就像贪心算法的问题——取决于初值,沿着梯度优化最终只能得到局部最优解。这相当于最终能达到的高度在一开始就被决定了。

破解这种局面的算法之一是模拟退火。模拟退火算法的理念是在初期引入随机性,随着迭代次数的增加而减小随机因子,这样最终收敛到全局最优解的机率更大。跟随这个理念来考虑职业的选择,我决定跳出现有的路径,哪怕是这个路径的前方一片光明,现在只是为了体验不同的选择。

模拟退火

以上是模拟退火算法寻求全局最值,图片来自维基百科

下一篇:在Google的这四年(五)