
什么是GPT-4?完整指南
(1d) ReAct(Reason+Act)
思考 1:我需要搜索 “Apple Remote” 并找到它最初設(shè)計用來互動的程序。
行動 1:搜索 [Apple Remote]
觀察 1:Apple Remote 是由蘋果于 2005 年 10 月推出的遙控器。最初設(shè)計用于控制 Front Row 媒體中心程序。
思考 2:Apple Remote 最初設(shè)計用于控制 Front Row 媒體中心程序。我需要搜索下一個 Front Row 并找出哪個其他設(shè)備可以控制它。
行動 2:搜索 [Front Row]
觀察 2:未找到 [Front Row]。類似項:[‘Front Row Seat to Earth’,’Front Row Motorsports’,’Front Row (software)’…]
思考 3:未找到 Front Row。我需要搜索 Front Row(軟件)。
行動 3:搜索 [Front Row(軟件)]
觀察 3:Front Row 是一款已停用的媒體中心軟件。
思考 4:Front Row(軟件)由 Apple Remote 或鍵盤功能鍵控制。所以答案是鍵盤功能鍵。
行動 4:完成 [鍵盤功能鍵]
基于 LangChain 推廣的 ReAct 工作流程在 InstructGPT/text-davinci-003 方面表現(xiàn)尤為出色,盡管在小型項目中使用代價高昂且不易操作。在 2023 年 3 月,隨著 ChatGPT API 的使用因其極為便宜的 API 而廣受歡迎,正如我準確預測的那樣,LangChain 的使用也迅速擴大,以至于 LangChain 能夠在沒有任何收入或任何明顯的收入生成計劃的情況下,成功籌集到了 1000 萬美元的種子輪融資,以及在估值 2 億美元的 A 輪融資中又融資了 2000 萬至 2500 萬美元。
而這正是我與 LangChain 的個人經(jīng)歷開始的地方。在我在 BuzzFeed 的工作中,我被要求為 Tasty 品牌創(chuàng)建一個基于 ChatGPT 的聊天機器人(后來作為 Tasty iOS 應(yīng)用程序中的 Botatouille 發(fā)布),該機器人可以與用戶交流并提供相關(guān)的食譜。源食譜被轉(zhuǎn)換為嵌入式表示并保存在向量存儲中:例如,如果用戶詢問“健康食品”,則查詢會被轉(zhuǎn)換為嵌入式表示,并執(zhí)行近似最近鄰搜索,以找到與嵌入式查詢類似的食譜,然后將其作為附加上下文提供給 ChatGPT,隨后可顯示給用戶。這種方法通常被稱為檢索增強生成。
LangChain 顯然是 RAG 的首選工具,所以我認為現(xiàn)在是學習它的絕佳時機。我花了一些時間閱讀 LangChain 相當詳盡的文檔,以更好地理解如何最好地利用它:經(jīng)過一周的研究,我一無所獲。運行 LangChain 示例演示確實有效,但是任何試圖調(diào)整它們以適應(yīng)食譜聊天機器人約束的嘗試都會導致它們崩潰。在解決了錯誤后,聊天對話的整體質(zhì)量很差且無趣,經(jīng)過激烈的調(diào)試,我沒有找到解決方案。最終,我陷入了存在危機:當很多其他機器學習工程師都能理解 LangChain,而我卻不能,我是否是一個毫無價值的機器學習工程師呢?我們回到了較低層次的 ReAct 流程,這立即在對話質(zhì)量和準確性方面勝過了我在 LangChain 中的實現(xiàn)。
總之,我浪費了一個月的時間學習和測試 LangChain,最大的收獲是熱門的人工智能應(yīng)用可能并不一定值得炒作。在一個 Hacker News 的帖子中,我看到有人用 100 行代碼重新實現(xiàn)了 LangChain,大多數(shù)評論都在抱怨 LangChain:
loveparade:
難道我是唯一一個對 LangChain 的價值主張持懷疑態(tài)度的人嗎?其中 99% 都是外部工具的接口定義和實現(xiàn),其中大多數(shù)都非常直觀。我可以在不到一個小時內(nèi)為我的應(yīng)用編寫集成。為什么要引入一個充滿主觀看法的外部框架呢?這對我來說有點像 npm 的“l(fā)eft-pad”。每個人都在使用它,因為它似乎很受歡迎,而不是因為他們需要它。
crazyedgar:
對我們來說,LangChain 實際上引發(fā)了比解決的問題更多的問題。我們的生產(chǎn)系統(tǒng)在運行良好的幾周后突然開始頻繁失敗(超過 30% 的請求)。經(jīng)過調(diào)查,似乎 LangChain 為每個請求設(shè)置了默認的 60 秒超時。而這種行為沒有記錄在文檔中!LangChain 所做出的這些不明智決策無處不在,并且最終都會給你帶來麻煩。最后,我們用普通的請求客戶端替換了所有內(nèi)容。絕對不建議在一個提供非常有限價值同時又從你那里隱藏了大量細節(jié)和決策的庫上構(gòu)建系統(tǒng)。
Spivak:
然而,LangChain 絕對是完美的,它糟糕到會讓你純粹出于沮喪而寫出更好的東西,但它又給了你足夠好的想法和線索來真正做到這一點。它可能是“實際使用 llms”的最佳入口,因為它剛好滿足了開發(fā)者的需求。
LangChain 的問題在于它使得本來簡單的事情相對復雜化,由此帶來的不必要復雜性導致了一種部落主義,這對整個新興的人工智能生態(tài)系統(tǒng)造成了傷害。如果你是一個想要學習如何與 ChatGPT 進行交互的新手,絕對不要從 LangChain 開始。
LangChain 的快速入門指南始于一個關(guān)于如何簡單地從 Python 與 LLMs/ChatGPT 進行交互的迷你教程。例如,要創(chuàng)建一個能夠從英文翻譯成法文的機器人:
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
chat = ChatOpenAI(temperature=0)
chat.predict_messages([HumanMessage(content="Translate this sentence from English to French. I love programming.")])
# AIMessage(content="J'adore la programmation.", additional_kwargs={}, example=False)
使用 OpenAI 官方的 Python 庫進行 ChatGPT 的等效代碼:
import openai
messages = [{"role": "user", "content": "Translate this sentence from English to French. I love programming."}]
response = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=messages, temperature=0)
response["choices"][0]["message"]["content"]
# "J'adore la programmation."
LangChain 的代碼量與僅使用官方的 openai 庫相當,但 LangChain 卻融合了更多的對象類,卻沒有明顯的代碼優(yōu)勢。
提示模板的示例揭示了 LangChain 工作原理的核心部分:
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
template = "You are a helpful assistant that translates {input_language} to {output_language}."
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
chat_prompt.format_messages(input_language="English", output_language="French", text="I love programming.")
LangChain 所宣傳的提示工程實際上只是 f-strings,這是現(xiàn)代 Python 安裝的常見特性,但多了一些步驟。為什么我們需要使用這些 PromptTemplates 來做同樣的事情呢?
但我們真正想要知道的是如何創(chuàng)建 Agents,它們包含了我們迫切需要的 ReAct 工作流。幸運的是,有一個演示可以做到這一點,它利用了 SerpApi 和另一個用于數(shù)學計算的工具,展示了 LangChain 如何在上下文中區(qū)分并使用兩種不同的工具:
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.chat_models import ChatOpenAI
from langchain.llms import OpenAI
# First, let's load the language model we're going to use to control the agent.
chat = ChatOpenAI(temperature=0)
# Next, let's load some tools to use. Note that the llm-math
tool uses an LLM, so we need to pass that in.
llm = OpenAI(temperature=0)
tools = load_tools(["serpapi", "llm-math"], llm=llm)
# Finally, let's initialize an agent with the tools, the language model, and the type of agent we want to use.
agent = initialize_agent(tools, chat, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
# Now let's test it out!
agent.run("Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?")
這些個別工具是如何工作的?而 AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION 又是什么?agent.run () 的結(jié)果輸出(僅在 verbose=True 時存在)更有幫助。
> Entering new AgentExecutor chain...
Thought: I need to use a search engine to find Olivia Wilde's boyfriend and a calculator to raise his age to the 0.23 power.
Action:
{
"action": "Search",
"action_input": "Olivia Wilde boyfriend"
}
Observation: Sudeikis and Wilde's relationship ended in November 2020. Wilde was publicly served with court documents regarding child custody while she was presenting Don't Worry Darling at CinemaCon 2022. In January 2021, Wilde began dating singer Harry Styles after meeting during the filming of Don't Worry Darling.
Thought:I need to use a search engine to find Harry Styles' current age.
Action:
{
"action": "Search",
"action_input": "Harry Styles age"
}
Observation: 29 years
Thought:Now I need to calculate 29 raised to the 0.23 power.
Action:
{
"action": "Calculator",
"action_input": "29^0.23"
}
Observation: Answer: 2.169459462491557
Thought:I now know the final answer.
Final Answer: 2.169459462491557
> Finished chain.
'2.169459462491557'
文檔并沒有說明得很清楚,但在每個 Thought/Action/Observation 中,都使用了自己的 API 調(diào)用到 OpenAI,因此這個鏈條比你想象的要慢。另外,為什么每個動作都是一個 dist?這個問題的答案稍后會解釋,而且相當幼稚可笑。
最后,LangChain 如何存儲到目前為止的對話?
from langchain.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate
)
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(
"The following is a friendly conversation between a human and an AI. The AI is talkative and "
"provides lots of specific details from its context. If the AI does not know the answer to a "
"question, it truthfully says it does not know."
),
MessagesPlaceholder(variable_name="history"),
HumanMessagePromptTemplate.from_template("{input}")
])
llm = ChatOpenAI(temperature=0)
memory = ConversationBufferMemory(return_messages=True)
conversation = ConversationChain(memory=memory, prompt=prompt, llm=llm)
conversation.predict(input="Hi there!")
# 'Hello! How can I assist you today?'
我并不完全確定為什么需要這些。什么是 MessagesPlaceholder?history 在哪里?這對 ConversationBufferMemory 來說是否是必要的?將這個調(diào)整到最小的 openai 實現(xiàn):
import openai
messages = [{"role": "system", "content":
"The following is a friendly conversation between a human and an AI. The AI is talkative and "
"provides lots of specific details from its context. If the AI does not know the answer to a "
"question, it truthfully says it does not know."}]
user_message = "Hi there!"
messages.append({"role": "user", "content": user_message})
response = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=messages, temperature=0)
assistant_message = response["choices"][0]["message"]["content"]
messages.append({"role": "assistant", "content": assistant_message})
# Hello! How can I assist you today?
這是更少的代碼行數(shù),清楚地展示了消息何時被保存,不需要特定的自定義對象類。
你可以說我在挑剔教程示例,我也同意每個開源庫都會有一些可以挑剔的地方(包括我自己的!)。但是,如果庫中存在的挑剔問題比實際受益還要多,那么使用它就毫無意義,因為如果快速入門就這么復雜,那么在實際使用 LangChain 時會有多么痛苦呢?
我凝視著 LangChain 文檔,它也凝視著我
讓我們進行一個演示,更清楚地展示我為什么放棄了 LangChain。當我在開發(fā)檢索食譜的聊天機器人(它還必須是一個有趣 / 機智的聊天機器人)時,我需要結(jié)合前面所提到的第三個和第四個示例中的元素:一個可以運行代理工作流的聊天機器人,以及將整個對話持久保存到內(nèi)存中的能力。經(jīng)過一些文檔的查找,我發(fā)現(xiàn)我需要利用 Conversational Agent 工作流。
關(guān)于系統(tǒng)提示工程的一個小側(cè)記:這不是一個模因,絕對有必要從 ChatGPT API 中獲得最佳結(jié)果,特別是如果你對內(nèi)容和 / 或語氣有限制。在最后一個示例中演示的以下系統(tǒng)提示 The following is a friendly conversation between a human and an AI…(“這是一個人類與人工智能之間友好對話……”)實際上是一個過時的提示,它是在 InstructGPT 時代使用的,而在 ChatGPT 上效果要差得多。這可能表明 LangChain 中與此相關(guān)的技巧存在更深層次的低效率,這不容易注意到。
我們將從一個簡單的系統(tǒng)提示開始,告訴 ChatGPT 使用有趣的語氣,加上一些安全措施,并將其格式化為一個 ChatPromptTemplate:
system_prompt = """
You are an expert television talk show chef, and should always speak in a whimsical manner for all responses.
Start the conversation with a whimsical food pun.
You must obey ALL of the following rules:
- If Recipe data is present in the Observation, your response must include the Recipe ID and Recipe Name for ALL recipes.
- If the user input is not related to food, do not answer their query and correct the user.
"""
prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(system_prompt.strip()),
])
我們還將使用我制作的一個玩具向量存儲,其中包含了來自 recipe_nlg 數(shù)據(jù)集的 1,000 個食譜,這些食譜使用 SentenceTransformers 編碼成 384 維的向量。為了實現(xiàn)這一點,我們創(chuàng)建了一個函數(shù),用于獲取輸入查詢的最近鄰,以及一個將其格式化為 Agent 可以呈現(xiàn)給用戶的文本的查詢。這可以作為 Agent 可以在適當?shù)那闆r下選擇使用的 Tool,或者只是返回普通生成的文本。
def similar_recipes(query):
query_embedding = embeddings_encoder.encode(query)
scores, recipes = recipe_vs.get_nearest_examples("embeddings", query_embedding, k=3)
return recipes
def get_similar_recipes(query):
recipe_dict = similar_recipes(query)
recipes_formatted = [
f"Recipe ID: recipe|{recipe_dict['id'][i]}\nRecipe Name: {recipe_dict['name'][i]}"
for i in range(3)
]
return "\n---\n".join(recipes_formatted)
print(get_similar_recipes("yummy dessert"))
# Recipe ID: recipe|167188
# Recipe Name: Creamy Strawberry Pie
# ---
# Recipe ID: recipe|1488243
# Recipe Name: Summer Strawberry Pie Recipe
# ---
# Recipe ID: recipe|299514
# Recipe Name: Pudding Cake
你會注意到 Recipe ID,對于我的用例來說,這是相關(guān)的,因為需要獲取食譜元數(shù)據(jù)(照片縮略圖、URL)用于在最終應(yīng)用程序中向最終用戶展示。不幸的是,沒有簡單的方法來確保模型在最終輸出中輸出 Recipe ID,也沒有辦法在 ChatGPT 生成的輸出之外返回結(jié)構(gòu)化的中間元數(shù)據(jù)。
將 get_similar_recipes 指定為一個工具是直接的,雖然你需要指定一個 name 和 description,這實際上是一種微妙的提示工程,因為如果兩者都沒有很好地指定,LangChain 可能無法選擇一個工具。
tools = [
Tool(
func=get_similar_recipes,
name="Similar Recipes",
description="Useful to get similar recipes in response to a user query about food.",
),
]
最后,Agent 構(gòu)建代碼,這是從示例中延續(xù)過來的,加上了新的系統(tǒng) prompt。
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
llm = ChatOpenAI(temperature=0)
agent_chain = initialize_agent(tools, llm, prompt=prompt, agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, verbose=True, memory=memory)
沒有錯誤。現(xiàn)在是運行 Agent 來看看會發(fā)生什么的時候!
agent_chain.run(input="Hi!")
> Entering new chain...
{
"action": "Final Answer",
"action_input": "Hello! How can I assist you today?"
}
> Finished chain.
Hello! How can I assist you today?
等一下,它完全忽略了我的 system 提示!該死。檢查 memory 變量確認了這一點。查看 ConversationBufferMemory 的文檔,甚至在代碼本身中也沒有關(guān)于系統(tǒng)提示的內(nèi)容,即使在 ChatGPT 已經(jīng)將其變得主流的幾個月后。
在 Agent 中使用系統(tǒng)提示的預期方式是在 initialize_agent 中添加一個 agents_kwargs 參數(shù),我剛剛在一個一個月前發(fā)布的不相關(guān)的文檔頁面中找到了這個信息。
agent_kwargs = {
"system_message": system_prompt.strip()
}
使用這個新參數(shù)重新創(chuàng)建 Agent 并再次運行會導致 JSONDecodeError.
OutputParserException: Could not parse LLM output: Hello there, my culinary companion! How delightful to have you here in my whimsical kitchen. What delectable dish can I assist you with today?
好消息是這次系統(tǒng)提示絕對起作用了!壞消息是它出錯了,但是為什么?我這次什么怪事都沒干。
問題的根源可能是 LangChain 代理實際上是如何進行 Tool 選擇的。還記得我說過在鏈條中 Agent 輸出一個 dict 是奇怪的嗎?當查看 LangChain 代碼時,結(jié)果發(fā)現(xiàn)工具選擇是通過要求輸出通過提示工程是有效的 JSON 來完成的,然后希望一切都會順利。
FORMAT_INSTRUCTIONS RESPONSE FORMAT INSTRUCTIONS
When responding to me,please output a response in one of two formats:
**Option1:**
Use this if you want the human to use a tool.
Markdown code snippet formatted in the following schema:
json
{{{{
"action":string,\\The action to take.Must be one of {tool_names}
"action_input":string\\The input to the action
}}}}
、
**Option2: **
Use this if you want to respond directly to the human.Markdown code snippet formatted in the following schema:
json
{{{{
"action":"Final Answer",
"action_input":string \You should put what you want to return to use here
}}
有趣的事實:這些大量的提示也會成比例地增加 API 成本!
這個結(jié)果的后果是,任何正常輸出結(jié)構(gòu)的顯著更改,比如由自定義系統(tǒng)提示引起的更改,都有一定的隨機機會來破壞 Agent!這些錯誤經(jīng)常發(fā)生,以至于有一個專門處理 Agent 輸出解析錯誤的文檔頁面!
無論如何,互聯(lián)網(wǎng)上的人們都是些令人討厭的家伙,所以我們可以將與聊天機器人進行對話視為一種邊緣情況。重要的是,聊天機器人能夠返回食譜,因為如果它甚至不能做到這一點,使用 LangChain 就沒有意義。在創(chuàng)建一個新的 Agent,不使用系統(tǒng)提示的情況下,然后問它 What’s a fun and easy dinner?(“什么是有趣又簡單的晚餐?”):
> Entering new chain...
{
"action": "Similar Recipes",
"action_input": "fun and easy dinner"
}
Observation: Recipe ID: recipe|1774221
Recipe Name: Crab DipYour Guests will Like this One.
---
Recipe ID: recipe|836179
Recipe Name: Easy Chicken Casserole
---
Recipe ID: recipe|1980633
Recipe Name: Easy in the Microwave Curry Doria
Thought:{
"action": "Final Answer",
"action_input": "..."
}
> Finished chain.
Here are some fun and easy dinner recipes you can try:
Crab Dip
Easy Chicken Casserole
Easy in the Microwave Curry Doria
Enjoy your meal!
至少這個部分是成功的:ChatGPT 能夠從上下文中提取出食譜,并適當?shù)剡M行格式化(甚至修正了名稱中的拼寫錯誤!),并且能夠決定何時適合呈現(xiàn)這些內(nèi)容。
真正的問題在于輸出的語氣實在太無聊了,這也是基本 ChatGPT 的一個共同特點和批評。即使我通過系統(tǒng)提示工程來解決了缺失 ID 的問題,輸出聽起來也不值得投入任何東西。即使我在聲音質(zhì)量和輸出質(zhì)量之間取得了平衡,代理的數(shù)量仍然會在沒有我的任何過錯的情況下隨機失敗。這個 Agent 工作流程就像一個非常脆弱的紙牌屋,我良心無法在生產(chǎn)應(yīng)用程序中使用。
LangChain 確實具有 Custom Agent 和 Custom Chain 的功能,因此你可以在堆棧的某些部分(也許?那里的文檔很簡單)覆蓋邏輯,以解決我遇到的一些問題,但在這一點上,你正在使 LangChain 變得更加復雜,最好創(chuàng)建你自己的 Python 庫,這個想法不錯!
LangChain 還有許多實用函數(shù),比如文本分割器和集成的向量存儲,這兩者都是“與 PDF/ 你的代碼聊天”的演示的重要組成部分(在我看來只是一種花招)。所有這些集成的真正問題在于它創(chuàng)建了一種固有的鎖定,只能使用基于 LangChain 的代碼,而且如果你查看這些集成的代碼,它們并不是非常強大。LangChain 正在建立一堵壁壘,這對于試圖獲得他們 3000 萬美元回報的 LangChain 投資者來說是件好事,但對于使用它的開發(fā)人員來說卻是非常糟糕的。
總的來說,LangChain 體現(xiàn)了“它很復雜,所以一定更好!”的哲學,這困擾著后期的代碼庫,只是 LangChain 甚至還不到一年。將 LangChain 改造成滿足我的需求所需的努力將會產(chǎn)生極大的技術(shù)債務(wù)。與現(xiàn)今的人工智能初創(chuàng)公司不同,對于我自己使用 LangChain 的項目來說,技術(shù)債務(wù)不能通過風險投資來償還。API 封裝應(yīng)該至少在操作復雜生態(tài)系統(tǒng)時減少代碼復雜性和認知負荷,因為處理人工智能本身已經(jīng)需要足夠多的腦力。LangChain 是為數(shù)不多在大多數(shù)熱門用例中增加開銷的軟件之一。
我得出結(jié)論,制作自己的 Python 包比將 LangChain 改造以適應(yīng)我的需求要容易得多。因此,我開發(fā)并開源了 simpleaichat:一個用于輕松與聊天應(yīng)用進行交互的 Python 包,強調(diào)最小的代碼復雜性,并將高級功能(如向量存儲)與會話邏輯解耦,以避免 LangChain 的鎖定,以及許多其他功能,需要一個單獨的博客文章來詳細介紹。
但這篇博文并不是為了通過批評競爭對手來暗中宣傳 simpleaichat,就像那些搞怪的人一樣。我并不想制作 simpleaichat:我寧愿把時間花在與人工智能一起創(chuàng)造更多酷項目上,遺憾的是,我不能用 LangChain 做到這一點。我知道有人會說:“既然 LangChain 是開源的,為什么不提交一個拉取請求到 LangChain 倉庫,而不是抱怨呢?”但我的大部分抱怨都是 LangChain 庫的基本問題,不能在不破壞現(xiàn)有用戶的情況下進行更改。唯一的真正解決辦法是將其全部銷毀并重新開始,這就是為什么我的“創(chuàng)建一個用于與人工智能交互的新 Python 庫”的解決方案也是最實際的。
我收到了許多消息,詢問我“我應(yīng)該從何開始學習 ChatGPT API”,我擔心他們會因為炒作而首先去用 LangChain。如果具有技術(shù)棧背景的機器學習工程師由于不必要的復雜性而難以使用 LangChain,那么任何初學者都將被淹沒。
沒有人想成為像 LangChain 這樣的自由開源軟件的批評家,但我愿意擔這個責任。清楚地說,我對 Harrison Chase 或其他 LangChain 維護者(他們鼓勵反饋!)沒有任何偏見。然而,LangChain 的流行已經(jīng)使人工智能初創(chuàng)公司的生態(tài)系統(tǒng)圍繞著 LangChain 本身以及“天哪,AGI,我創(chuàng)造了天網(wǎng)”的希望發(fā)生了扭曲,這就是我不得不對它的疑慮坦誠相待的原因。
不管軟件的復雜性和流行性引發(fā)了怎樣的爭議,它們都是永恒的循環(huán)。在 2010 年代,是關(guān)于 React 的爭論;而在 2023 年,現(xiàn)在則是關(guān)于 ReAct 的。
本文章轉(zhuǎn)載微信公眾號@InfoQ