
如何用AI進行情感分析
2001 年,我還在上大二時,微軟發布了 .Net framework 的第一個 RC。當時 .Net 聲勢浩大,微軟在各個主要高校組織了夏令營,來選拔來年 Microsoft Asia 開發者大會的團隊。當時我印象深刻的是,跟隨著 .Net 一起新鮮出爐的 WSDL(Web Service Description Language),它「第一次」(也許)以公開協議的方式來描述通用的HTTP 客戶端和服務器之間的 API。在 WSDL 的約定下,API 的請求和響應以 XML SOAP 的形式封裝。
在那個狂野的,沒有 API 的概念的時代,WSDL 簡直就是一股清流。可惜它思想太超前,服務描述太繁雜,使得一個非常簡單的 API 動輒生成成百上千行 XML 格式的 WSDL。在那個客戶端和服務器能力還十分有限的年代,WSDL 幾乎沒有激出任何水花,就被扔到了歷史的故紙堆中。
可能早期微軟把太多的賭注放在了曲高和寡的 WSDL(以及服務發現協議 UDDI)上,其主打做 web 開發的 ASP.Net 一直不溫不火,根本無法與紅遍天的 PHP 相提并論。在 2005 年之前,可以說,(在 web 世界里),PHP 是宇宙中最好的語言。
然而,成也蕭何敗也蕭何,脫胎于 Web 開發的 PHP,與 Web 的親和性是其優勢,也是其后續沒落的原因 —— 畢竟,當 Web 軟件越來越復雜,需要跟越來越多的 web 以外的世界(比如操作系統)打交道時,跟其他通用的腳本語言,如 Python/Ruby 相比,PHP 就盡顯劣勢。
尤其是,當 Ruby on Rails(以下簡稱 rails)這個引領一個時代的 web 框架橫空出世后,PHP 尷尬的發現,自己的優勢,可能就只剩下多年來積攢的生態系統,以及在這個生態下滋養著的一大堆開發者了。
rails 是一個足以載入史冊的框架:它把軟件開發中的很多非常有益的概念、模式和思想(包括但不限于 ORM,CoC,MVC 等)糅合在自己體內,構建了一個強大同時非常易用的 web 開發系統。在 rails 下,哪怕你是個 web 開發的小白,在學習了 rails 的開發文檔后,也能很快撰寫出一套讓很多 web 開發老鳥艷羨的系統。在 rails 諸多創新之中,要數 ActiveRecord 最為經驗,它以簡潔優雅的表述,顛覆了人們傳統上對數據庫的認知,并且幾乎憑借一己之力,把 ORM 捧上了神壇。
隨著 rails 一起成長的還有 XMLHttp object (俗稱 Ajax)的標準化,以及 JSON 的廣泛使用。其中,Google 通過其旗下的 gmail / google maps 大大促進了人們對 Ajax 的認知,而 PHP5 和 rails 3 則將 JSON 在廣大開發者中推廣開來,使其逐漸取代笨拙低效的 XML。有意思的是,Ajax 最初是 Asynchronous Javascript And XML,JSON 普及后,這個 XML 再也沒人提及。
rails 的成功催生了一系列迷弟迷妹 —— 各個語言的,無論是高仿 rails,或者受 rails 啟發的框架如雨后春筍般冒出,好不熱鬧。這其中,光是我深度使用過的框架就有:symfony,django 和 Phoenix framework。由 rails 刮起的 ORM 之風愈演愈烈,它幾乎成為了 web 開發者訪問數據庫的唯一標準。漸漸的,存儲過程(stored procedure / function)被雪藏,觸發器(trigger)被遺忘,數據庫復雜而迷人的權限管理被棄之不顧,取而代之的是用一個幾乎具有 root 權限的用戶來連接數據庫,而權限的管理全部被前移到了應用層。這和 ORM 所倡導的「一套代碼處理多種數據庫」有莫大的聯系。事實上,ORM 帶給大家切換數據庫的好處,可能僅限于開發環境用 sqlite,生產環境用 postgres 這樣的便利。但從管理的角度,ORM 讓開發者繞過 DBA(或者干脆不要 DBA)進行快速開發,對于小型項目,可以高效開發,且不需要構建數據庫領域的專有技能,畢竟培養一個 web 工程師,兩三個月的訓練營就可以讓一個素人很好掌握開發框架,進行「高效」 CRUD 開發;而培養一個合格的 DBA,需要整個計算機體系知識的沉淀。早年間 DBA 還是個熱門的職位,后來在 rails 以及其一眾小弟的推波助瀾下,DBA 幾乎在中小型企業中銷聲匿跡。
現在回過頭來看,2010年前后,也就是我創業做途客圈前后,算是近年來少有的互聯網創新領域集中爆發的幾年。這其中,最大的功臣要數喬幫主 2007 年推出的驚世駭俗的「三個」產品:初代 iPhone。它完全顛覆了我們對手機的認知,顛覆了對輸入的認知,以及,顛覆了我們的生活。
隨后,大獲成功的 iPhone 4(及 4s)真正把我們的生活扯入了移動互聯網時代 —— 作為當時最成功最流行的 3G 手機,iPhone 4讓移動應用進入到主流用戶的視野。由于移動應用擁有自己的 UI 層,不像瀏覽器那樣,UI 層是由服務器返回的 HTML 渲染出來,因而移動端和服務器之間有著強烈的對簡潔高效且標準化的 API 層的需求。在這種需求的催生下,REST(Representational state transfer)這個當時已不新鮮的概念漸漸從象牙塔走入了工業界。人們發現,與其自己隨機指定一套 HTTP API 的規約,不如遵循 HTTP/1.1 規范,讓 API 的表述和規范靠攏。這個時期,各個框架要么開始內建對 RESTful API 的支持,要么在框架之上,獨立出一套專門為 API 優化的框架,比如 2012 年就比較成熟的 django REST framework:
也許是受到了移動互聯網的沖擊,也許是看到了客戶端和服務器彼此隔離帶來的巨大好處,web 開發也漸漸向 REST API 靠攏。在早期的 backbone.js 的引領下,web app 的 API 化在 react 發布后迅速升溫,并在后續的幾年得到了主流開發者的認可。到目前為止,純服務器渲染返回 HTML 的 web 應用可能只剩下半壁江山。
這個時期,如雨后春筍般綻放的眾多 REST API framework 給開發者帶來的巨大好處是,你即便不掌握 HTTP/1.1 協議的細節,也可以做出像樣的 API,來處理客戶端和服務器間的交互。
然而,并不是所有的 API 框架都足夠嚴謹,足夠遵循協議本身。很多 API 框架,在處理復雜的協議流程時,要么會有自相矛盾的處理,要么把這些細節完全交由開發者處理。然而,你如何保證只熱衷于進行 CRUD 的開發者能夠正確使用 ETag 作為樂觀鎖(optimistic locking)進行條件更新(conditional update)呢?
在 web 世界不為人知的角落,Erlang 的 webmachine 盡著最大的努力來確保 API 的處理符合 HTTP 協議。得益于 erlang 強大的 pattern matching 的能力,webmachine 在內部構建了一張龐大的決策樹,涵蓋了 API 處理的每一個細節,連每個錯誤返回的狀態碼都精益求精。
我曾經一度把玩過 liberator,相對于我當時在生產環境使用的比較流行的 eve 和 django rest framework 來說, liberator 真的是優秀很多。
然而,移動互聯網不是小眾語言和小眾框架的戰場。何況,API 畢竟是客戶端和服務器共同的約定,在那個年代,服務端的嚴謹會給客戶端帶來不小的困惑:相較于 412 Preconditional failed 而言,客戶端工程師更鐘情于一招鮮吃遍天的 400 Bad request。
由于在途客圈和 Juniper web security team 有了不少對 API 開發的思考和沉淀,我一直有心做一個自己的 API 開發框架。在加入 Tubi,理順我們當下的 API 結構后,我便以 eve 和 liberator 為藍圖,nodejs restify 為基石,嘗試著構建了一個 UAPI 系統,目的是以 pipeline 的形式處理 API 的流程,讓公司的 nodejs 開發者只需要專注在業務邏輯,其它的交由框架完成:
UAPI 算是個成功的 API 系統,它在 Tubi 一直使用了六年多,直到現在還在局部使用。對客戶端而言,它最大的好處是輸入和輸出都可以強制類型(如果定義了 validators 的話),這樣,不符合要求的輸入會在 API 處理流程很早的時候就被捕獲,進而返回詳盡的錯誤。
在 UAPI 演進的過程中,我也感受到了它的諸多局限和問題。其中最大的問題是:框架的使用者是開發者,而開發者如果沒有得到充足的培訓,會遺漏、誤用、濫用框架的某些能力。比如在 UAPI 中,API 的類型安全不是強制的,因而有的 API 在一開始對 Request 中的各個部分做了類型檢查,但隨著 API 的迭代,往往新添加的 HTTP 頭,并沒有妥善定義相應的類型檢查,于是開發者在業務邏輯中東一塊西一塊做各種校驗,最終導致不優雅的,甚至混亂的表達。
UAPI 的詳情我就不展開了,感興趣的可以參考我之前的系列文章:再談 API 的撰寫 – 架構。
也許在 UAPI 上我犯下的最大的錯誤,就是沒有強制類型檢查,把是否需要類型安全的選擇交給了開發者。
并不只有我自己有類型安全的切膚之痛,似乎整個行業都發現了 RESTful API 在這一點上的不完善。2015 年,facebook 首先用開源的內部項目 GraphQL 向業界打出了意圖取代 RESTful API 的一記重拳。GraphQL 從輸入和輸出入手,在 HTTP 協議之上定義了一套查詢語言 —— 客戶端和服務器之間需要定義好支持的 query / mutation / subscription 的 schema,以及輸入和輸出數據結構的 type。
GraphQL 提出了一個看待 API 的全新視角:客戶端使用者可以根據需要靈活定義他們想查詢的數據,而不需要看服務端老爺們的臉色。在固執的 RESTful API 的原教旨主義者眼里,API 應該嚴格對應資源,因而一個 app 頁面如果包含三種不同的資源,那么它就要訪問三個不同的 API 來獲得結果。對客戶端來說,這額外多了兩個浪費用戶寶貴等待時間的 roud trip,為什么不能一個查詢就獲得我想要的數據,且僅包含我想要的數據呢?
這個想法很有創意,但它忽視了靈活性帶來的可能并不值得的復雜性。GraphQL 的理想情況一直沒有很好地達成,因為服務端不可能為一個多層隨意嵌套的查詢去準備數據。同時 GraphQL 還有其他很多設計上考慮不周的問題,其中最讓人詬病的是,對 HTTP 協議的無視,也就導致整個 HTTP 生態和 GraphQL 工作地很別扭,還有查詢時 n+1 的問題(data loader 只是個特定場景的解決辦法)。
其實仔細想想,GraphQL 并沒有領先到足以完全讓大家告別 RESTful API 的地步。其實 RESTful 服務器可以構建 proxy API 來訪問若干其它 API,來解決一個 round trip 就能滿足客戶端的需求,同時也可以使用 partial response 來讓客戶端精確指定它想要的數據。這樣下來,GraphQL 最重要的優勢便蕩然無存。
2016 年,google 開源了 gRPC。它使用 protobuf IDL 來解決輸入輸出的類型安全問題,并且采用 HTTP/2 來支持應用層的多路復用(multiplex)。gRPC 在設計時瞄準的就是 server-server 的使用場景,因而它可以使用二進制數據來達到最好的效率。由于我們這里只著重談 client/server 的 API 演進,就不展開談 gRPC。
2017 年,OpenAPI v3 問世,REST 的世界終于也有了自己的類型安全。然而 OpenAPI 并不強制輸入輸出的類型安全,這跟 UAPI 有同樣的問題:隨著公司 OpenAPI spec 的不斷迭代,API 中某些新添加的字段,很容易被忽略,日積月累下來,問題會越來越多。
類型安全對 API 系統的意義不僅僅是輸入輸出有更加嚴格的校驗,錯誤的輸入能在很早的時候就被發現這么簡單。它還打開了一扇新的大門:代碼生成。無論是 GraphQL / gRPC,還是 OpenAPI,它們都可以根據 schema 生成客戶端 SDK,甚至服務端的 stub 代碼。
當然,寫 schema 本身是一件很痛苦的事情,尤其是對于我們這些能寫代碼就不想寫文檔的開發者。于是,人們開始追尋取巧的辦法:可不可以只寫代碼,然后通過代碼來生成相應的 schema?
schema first, or code first, this is a question.
大部分支持 GraphQL 或者 OpenAPI 的框架遵從程序員的本性,讓你可以專注于寫代碼,順帶生成相應的 schema。這是典型的 code first 的思維。
而 schema first 的代表要數 gRPC —— 你撰寫 protobuf 定義,相應的編譯器會替你生成代碼。
這兩種方案的背后,實際上是框架思維和編譯器思維的較量。
在我看來,code first 背后的框架思維,就像地心說,它一開始很簡單,很容易上手,但隨后你就不得不添加越來越多的本輪和均輪來對模型不斷校正,使其適應在發展變化中的正確性的保證。
而 schema first 背后的編譯器思維,就像日心說,是「少有人走的路」,(因為要寫解析器或者編譯器)開頭異常艱難,但一旦成型,日后會越來越輕松,只需在不斷拓展編譯器的邊界。
在使用過多種 code first 的框架來構建 GraphQL / OpenAPI 的系統后,我開始構思自己的下一個 API 開發工具:goldrin。
這一次,我的目標是:
這可能是我在 arcblock 的征途中,除了 forge 框架外,另一個很有意義的成就。這個項目的目標如此宏大,某種意義上說也是為了彌補開發人員的不足,所以很多時候,受限的資源反倒更能驅動創新。
在這個目標的驅動下,goldrin 實現了從一個類似 ansible 的,用來描述數據類型以及在數據類型上允許進行的操作的 schema,構建出相應的數據庫表的定義,GraphQL server 端實現,以及文檔的定義。這套系統最大的好處是:無論是客戶端開發者,還是后端開發者,都可以撰寫幾十行 YAML 就得到一個可以運行的,和數據庫緊密連接的 API playground。然后你可以在此基礎上不斷調整,讓 API 從原型一步步走到令人滿意的,可發布的版本,期間幾乎不用撰寫代碼(可能需要簡單的 mock resolver)。當 API 的接口成型后,我們可以再撰寫代碼,重載特定的 resolver,使其擁有更高效,更優雅的實現。
如果大家對此感興趣,可以 google:Use goldorin to build absinthe ecto enabled GraphQL APIs,看看我在 2018 年 ElixirConf 上的 lightening talk。很遺憾的是,由于當時我還想在 goldrin 中提供對 gRPC 的支持后再開源,導致這一項目一直沒有開源,直到我離開。對于這個項目,我沒有像 UAPI 那樣留下一個系列文章,只有一篇短文:思考,問題和方法。
如果說 goldrin 是一個被外部環境倒逼出來的急中生智,quenya,則更多像是我在無拘無束的條件下,把我之前做過的諸多系統回溯一下,集大成的找樂子項目。這一次,我試圖從 OpenAPI v3 spec 出發,構建一切可以自動化生成的代碼,甚至包括 API 的測試。
quenya 說實話,在思路上并沒有比 goldrin 進步多少,它的核心還是一個編譯器,從 OpenAPI 中挖掘各種有用的信息,生成相應的代碼。對此感興趣的同學,可以看我的這個系列文章:構建下一代 HTTP API – 總覽。
讓我們快進到 2020 年。
低代碼開發平臺(Low-code development platform)雖然在 2014 年就被作為一個正式的名稱被提出,但其開始打開局部市場,獲得大規模融資,以及進一步的發展,也就是近兩年的事情。低代碼的概念其實一直挺模糊,但大家的共識是:用戶可以無需太多編碼,通過「描述」其需求(可以在 GUI 上操作,也可以是撰寫某種簡單易懂的 schema),就能夠構建完全可使用的應用軟件。低代碼描繪了一個程序員之外的更廣泛的人群可以構建應用程序的美好世界。
然而,有應用程序的地方,就需要 API,而構建 API,則離不開開發者的參與。雖然過去二十年,API 開發的自動化程度已經大大提升,但我們還沒有到達一個可以完全自動生成 API 的階段。這還怎么低代碼?
如果我們重新審視 API 的作用,我們會發現,作為客戶端和服務端數據的橋梁,API 解析客戶端的請求,從服務端某個 data store(可能是數據庫,也可能是其他服務的數據等),獲取相應的數據,然后按照 API 的約定返回合適的結果。
既然 API 的目的是提供數據,而數據往往有其嚴苛的 schema,同時 API 的 schema 大多數時候就是數據 schema 的子集,那么,我們是不是可以從數據 schema 出發,反向生成 API 呢?
乍一看,這個思路和我之前做的 goldrin 類似,但 goldrin 定義了新的「語言」,由外及內地生成 API 以及數據的 schema,而這個想法是,以數據庫 schema 為單一數據來源,由內及外地生成 API schema,甚至 API 本身。
這并不是一個新的思想,早在 2015 年,postgREST 就開展了類似的嘗試,只是這種離經叛道的思路和那個 ORM 還如日中天的時代格格不入。在 DBA 幾乎絕跡于江湖后,有哪個初創企業會把自己的后端圍繞著一個特定的數據庫(postgres)構建,并且幾乎用盡這個數據庫每一個非標準的功能,完全不考慮可遷移性呢?
再加上 postgREST 是用 haskell 這樣一門小眾的語言開發,更使得好奇它的人多,而使用它的人少之又少。
簡單介紹一下 postgREST 的思路。使用 postgREST,開發者只需正常定義數據庫中的表,視圖,函數,觸發器等,并為它們的使用權限賦予相應的角色即可。postgREST 可以根據數據庫的 infoschema,掌握詳細的 metadata,并用這些 metadata 來驗證 API 的輸入,也就是 Request,如果驗證通過,會根據 Request 生成相應的 SQL 查詢,然后把結果序列化成客戶端需要的結構,以 Response 返回。舉個例子,對于這樣一個 API 請求:GET /people?age=gte.18&student=is.true
,postgREST 會驗證數據庫中包含 people 表或者視圖,并且其含有 age / student 這兩個字段,前者是整型,后者是布爾型。如果一切符合,并且用戶具備 people 表或者視圖的 SELECT 權限,那么它就會生成 select * from people where age >= 18 and student = true
這樣一條查詢,返回相應的 JSON(默認客戶端 accept: application/json
)。
postgREST 還跟 postgres 的 RLS(Row Level Security)深度綁定,來解決用戶個人信息安全訪問和更新的需求。比如用戶只能修改自己的帖子,但可以讀別人的帖子這樣的業務需求,如果沒有 RLS,很難從數據庫級別直接安全地實現。
postgREST 這樣一個小眾的工具進入到很多人的視野,還要歸功于 supabase B 輪八千萬美金的巨額融資。它為 postgREST 提供了 GUI,搖身一變成為 firebase 的挑戰者,DBaaS 新生代的翹楚。
另一個有著同樣思路,但采取了不同路徑的產品 Hasura,今年早些時候 C 輪融了一億美金。與 supabase 背后的 postgREST 不同的是,Hasura 把寶押在了 GraphQL。Hasura 試圖回答一個問題:有沒有可能把 GraphQL 的 query 一對一轉換成 SQL 語句?
我們知道 GraphQL 查詢會被編譯成 Graph AST,而 SQL 查詢會被編譯成 SQL AST,所以上述那個問題就變為:Graph AST 可以被安全高效地轉換成 SQL AST 么?
看看 Hasura 的天量融資,你就可以猜到,這條路走得通。撰寫自己的編譯器雖然是一條「少有人走的路」,但一旦走通,其迸發的能量是巨大的,而且有意想不到的效果。前面提到的 GraphQL 令人詬病的 n+1 的問題,在 Hasura 面前都不是是個事,因為引發 n+1 問題的嵌套查詢,翻譯成 SQL 就是一個 INNER JOIN,于是 n+1 問題就這么被悄無聲息地解決了。
那么,Hasura 是如何實現這一切的呢?我并沒有深入研究,然而當我打開 Hasura graphql-engine 的源碼,驚奇的發現,除了 20 多萬行 typescript/javascript 代碼,和 3 萬多行 golang 代碼外,它還有 13 萬行的 Haskell 代碼。莫非,Hasura 也從 postgREST 那里「偷師」?稍稍查詢一下,發現代碼中確實有一些 postgREST 的痕跡。
在仔細研讀了 postgREST 的用戶文檔后,我大概摸清了它的產品思路。于是我一時技癢,展開頭腦風暴,思考如果做一個類似的工具,我該怎么做?
首先,我并不喜歡 postgREST 的查詢方式,它的 DSL 在我看來有些蹩腳。我希望通過 x-fields 和 x-filter 這兩個 HTTP 頭,來實現 postgREST 里 querystring 所表達的內容:
對于 x-fields,它有略微復雜的,但繼承自 postgREST 的字段選擇語法,我可以使用一個 parser combinator(比如 Rust 下的 nom)來解析它,這樣就可以清晰地知道,字段名如何重命名,以及字段來自于哪張表(如果有 JOIN 的話)。x-filter 我還沒想好如何表述,但我覺得 SQL 中的表達式就夠用了。對于 x-filter,我們可以也用 parser combinator 來解析,或者干脆使用某個SQL 解析器(比如 Rust 下的 sqlparser)解析。解析出來的 metadata 可以和數據庫中的 infoschema 比對,來驗證請求的合法性,這一點和 postgREST 完全一致。最終,從 x-fields / x-filter 中解析出來的內容,連同 rang 頭(用于分頁)一起,就可以構建出一個完整的,合法的 SQL 查詢,最終得到返回的結果。
到目前為止,這個系統和 postgREST 如出一轍,沒什么了不起的。
平心而論,我覺得這樣的 API 系統,用于內部系統,還說得過去,但用于外部系統,就過于暴露數據 schema 的細節,同時讓 API 的接口和數據本身過于耦合。這樣一來帶有安全隱患(很容易被嗅探),二來不利于在 API 接口保持不變的情況下升級數據 schema。對此,postgREST 給出的答案是使用 view 來隔離 table schema 的細節,但我覺得還不夠完善。我需要一個能夠在外部看來,更加自然,更加簡單的 API。
在計算機的世界里,這樣的問題往往可以通過添加一個新的層級來實現。我并不需要改動已有的設計 —— 它對于內部系統來說還是相當不錯的設計,我只需要在這個設計之上,迭加一層。于是我有了這樣的思路:
開發者可以使用 CREATE API(我胡謅的新 SQL 語法) 來創建一個 API 的描述。這個 todos API,包含兩個參數:來自 auth header 的 jwt token,以及來自 querystring 里的 completed。API 的 metadata 中包含了一些詳盡的配置,以及 API 的參數如何作用到配置中。有了這樣的一種 API 配置,用戶可以用圖中更自然地方式訪問 API,而 API 自身沒有暴露任何數據庫的邏輯。
整個過程,比之前的方案多了個 API 的定義過程,由于使用的是描述性語言,所以,很難誤用,并且以后還能很方便的用 GUI 來表述,也算是一種低代碼了。
看到這里,有經驗的同學可能會質疑:API 的數據源又不止于數據庫,如果數據來源于 gRPC 服務器,那又該如何?
好問題!此刻我們需要修改 CREATE API 的描述,使其明確表達其數據源是什么。在下圖的例子里,數據源是 grpc_todos:
而 grpc_todos 由 CREATE SOURCE 來定義:
CREATE SOURCE grpc_todos WITH JSON({
"source": "wasm",
"wasm": {
"lib": "todo.wasm",
"fn": {
"name": "get_todos",
"args": [...],
"return": ".."
}
}
});
再一次地,我們看到,使用編譯器的思路去解決問題,是多么地舒服:我們可以不斷擴展新的語法,撰寫新的解析器去處理問題。這非常符合 open-close 原則。
這里 source 我使用 webassembly,并不是為了裝 B,而是我希望這樣的工具就像 postgREST 一樣,你不需要,也無法對其二次開發。如果需要擴展,那么 webassembly 或者 JS 就是最佳的選擇。它可以集成 wasmtime 來處理 webassembly,也可以集成 deno_core 來安全地支持 typescript/javascript 擴展。
以上關于第四次 API 工具的探索的一切不靠譜的想法,都只存在于我的腦海中,我的 excalidraw,以及我的 PPT 里。
本來這篇文章應該在上周末發表出來,可是我一時技癢,把周末可用的時間勻給了代碼實現,于是我在撰寫了(主要是通過 psql -E 偷師 psql 命令是如何查詢的)上百行 SQL,從postgres 中獲取關于 relation / function / columns / constraints 的 infoschema,將它們構建成 materialized view,然后利用這一信息,自動構建了簡單的,沒有任何安全限制的 API:
本文章轉載微信公眾號@程序人生