譯者 | 朱先忠
審校 | 重樓
本文旨在幫助你在使用自己定制的LLM代理還是使用現(xiàn)有LLM代理框架之間作出正確的選擇。
簡(jiǎn)介
首先,要感謝John Gilhuly對(duì)本文的貢獻(xiàn)。
當(dāng)下,人工智能代理暫時(shí)處在大休整時(shí)期。隨著多個(gè)新的AI開發(fā)框架的不斷出現(xiàn)和人們對(duì)該領(lǐng)域不斷進(jìn)行新的投資,現(xiàn)代人工智能代理正在克服不穩(wěn)定的初始階段,迅速取代RAG而成為實(shí)施重點(diǎn)。那么,2024年最終會(huì)成為什么樣的年份呢?是自主人工智能系統(tǒng)接管我們?nèi)斯頃鴮戨娮余]件、預(yù)訂航班、處理數(shù)據(jù),還是與任何其他年份一樣以相似方式執(zhí)行上述任務(wù)呢?
也許情況與前者一樣,但是要達(dá)到這種程度還有很多工作要做。任何構(gòu)建LLM代理的開發(fā)人員不僅必須選擇基礎(chǔ)開發(fā)設(shè)施——使用哪種模型、使用場(chǎng)景和架構(gòu)——還必須選擇要利用哪種開發(fā)框架。你是選擇長(zhǎng)期使用的LangGraph,還是新進(jìn)入市場(chǎng)的LlamaIndex工作流?還是你走傳統(tǒng)路線,自己編寫整個(gè)代碼呢?
這篇文章旨在讓這個(gè)選擇變得更容易一些。在過去的幾周里,我使用當(dāng)前主流的人工智能開發(fā)框架構(gòu)建了相同的LLM代理,以便在技術(shù)層面檢查每個(gè)框架的優(yōu)缺點(diǎn)。本文中涉及的每個(gè)代理的所有源代碼都可以在倉(cāng)庫(kù)地址處找到。
LLM代理類型
當(dāng)前,業(yè)界主要用于測(cè)試目的的LLM代理開發(fā)涉及到很多方面的內(nèi)容,例如函數(shù)調(diào)用、多種相關(guān)工具或技能、與外部資源的連接以及共享狀態(tài)或內(nèi)存,等等。
歸納起來看,幾乎所有LLM代理都具有以下功能:
- 回答知識(shí)庫(kù)中的問題。
- 與數(shù)據(jù)對(duì)話:回答有關(guān)LLM應(yīng)用程序遙測(cè)數(shù)據(jù)的問題。
- 分析數(shù)據(jù):分析檢索到的遙測(cè)數(shù)據(jù)中的高級(jí)趨勢(shì)和模式。
為了完成這些任務(wù),LLM代理需要具備三個(gè)基礎(chǔ)技能:使用產(chǎn)品文檔的RAG、在跟蹤數(shù)據(jù)庫(kù)上生成SQL和數(shù)據(jù)分析。一種典型的實(shí)現(xiàn)方案是,使用開源的Python包Gradio來快速構(gòu)建一個(gè)代理用戶界面,而LLM代理本身被構(gòu)造為聊天機(jī)器人。
基于定制代碼的代理(無框架方案)
開發(fā)LLM代理時(shí),你的第一個(gè)選擇很可能是完全跳過市場(chǎng)上現(xiàn)有框架,而完全由自己來構(gòu)建一個(gè)代理。在最開始著手做這種項(xiàng)目時(shí),這是我采用的方法。
純代碼架構(gòu)
下面展示的基于代碼的代理是由一個(gè)OpenAI驅(qū)動(dòng)的路由器組成的,該路由器使用函數(shù)調(diào)用來選擇要使用的正確技能。該技能完成后,它將返回路由器以調(diào)用另一個(gè)技能或者是對(duì)用戶作出響應(yīng)。
在這個(gè)代理中,始終保持一個(gè)持續(xù)的消息和響應(yīng)列表,在每次調(diào)用時(shí)將其完全傳遞到路由器中,以便在循環(huán)中保留相應(yīng)的上下文信息。
def router(messages):
if not any(
isinstance(message, dict) and message.get("role") == "system" for message in messages
):
system_prompt = {"role": "system", "content": SYSTEM_PROMPT}
messages.append(system_prompt)
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=skill_map.get_combined_function_description_for_openai(),
)
messages.append(response.choices[0].message)
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
handle_tool_calls(tool_calls, messages)
return router(messages)
else:
return response.choices[0].message.content
技能本身是在自己的類中定義的(例如GenerateSQLQuery),而所有這些技能信息共同保存在SkillMap類中。路由器本身只與SkillMap類交互,它使用SkillMap類實(shí)現(xiàn)來加載技能名稱、描述和可調(diào)用函數(shù)。這種方法意味著,向代理添加新技能就像將該技能編寫為自己的類一樣簡(jiǎn)單,然后將其添加到SkillMap類中的技能列表中。這里的想法是,在不干擾路由器代碼的情況下輕松添加新技能。
class SkillMap:
def __init__(self):
skills = [AnalyzeData(), GenerateSQLQuery()]
self.skill_map = {}
for skill in skills:
self.skill_map[skill.get_function_name()] = (
skill.get_function_dict(),
skill.get_function_callable(),
)
def get_function_callable_by_name(self, skill_name) -> Callable:
return self.skill_map[skill_name][1]
def get_combined_function_description_for_openai(self):
combined_dict = []
for _, (function_dict, _) in self.skill_map.items():
combined_dict.append(function_dict)
return combined_dict
def get_function_list(self):
return list(self.skill_map.keys())
def get_list_of_function_callables(self):
return [skill[1] for skill in self.skill_map.values()]
def get_function_description_by_name(self, skill_name):
return str(self.skill_map[skill_name][0]["function"])
總體而言,這種方法實(shí)施起來相當(dāng)簡(jiǎn)單,不過也存在一些挑戰(zhàn)。
純代碼代理方案的挑戰(zhàn)
第一個(gè)難點(diǎn)在于構(gòu)建路由器系統(tǒng)提示。通常,上述示例中的路由器堅(jiān)持自己生成SQL,而不是將其委托給合適的技能。如果你曾經(jīng)試圖不讓LLM做某事,你就會(huì)知道這種經(jīng)歷有多么令人沮喪;找到一個(gè)可用的提示需要進(jìn)行多輪調(diào)試。考慮到每個(gè)步驟的不同輸出格式也是很棘手的。由于我選擇不使用結(jié)構(gòu)化輸出,因此我必須為路由器和技能中每個(gè)LLM調(diào)用的多種不同格式做好準(zhǔn)備。
純代碼代理方案的優(yōu)點(diǎn)
基于代碼的方法提供了一個(gè)很好的基礎(chǔ)架構(gòu)和起點(diǎn),提供了一種學(xué)習(xí)代理如何工作的好方法,而不是依賴于主流框架中的現(xiàn)成的代理教程。盡管說服LLM的行為可能具有挑戰(zhàn)性,但代碼結(jié)構(gòu)本身足夠簡(jiǎn)單,可以使用,并且可能對(duì)某些使用場(chǎng)景也極有意義。有關(guān)這些使用場(chǎng)景的更多信息,請(qǐng)參閱接下來的分析。
LangGraph
LangGraph是歷史最悠久的代理框架之一,于2024年1月首次發(fā)布。該框架旨在通過采用Pregel圖結(jié)構(gòu)來解決現(xiàn)有管道和鏈的非循環(huán)性。LangGraph通過添加節(jié)點(diǎn)、邊和條件邊的概念來遍歷圖,使得在代理中定義循環(huán)變得更加容易。LangGraph構(gòu)建在LangChain之上,并使用LangChain框架中的對(duì)象和類型。
LangGraph架構(gòu)
LangGraph代理看起來與其原論文中的基于代碼的代理相似,但它后臺(tái)的實(shí)現(xiàn)代碼卻截然不同。LangGraph在技術(shù)上仍然使用“路由器”,因?yàn)樗ㄟ^函數(shù)調(diào)用OpenAI,并使用響應(yīng)繼續(xù)進(jìn)行新的步驟。然而,程序在技能之間移動(dòng)的方式完全不同。
tools = [generate_and_run_sql_query, data_analyzer]
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
def create_agent_graph():
workflow = StateGraph(MessagesState)
tool_node = ToolNode(tools)
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
)
workflow.add_edge("tools", "agent")
checkpointer = MemorySaver()
app = workflow.compile(checkpointer=checkpointer)
return app
這里定義的圖有一個(gè)用于初始OpenAI調(diào)用的節(jié)點(diǎn),上面稱為“agent”,還有一個(gè)用于工具處理步驟的節(jié)點(diǎn),稱為“tools”。LangGraph提供了一個(gè)名為ToolNode的內(nèi)置對(duì)象,它負(fù)責(zé)獲取可調(diào)用工具的列表,并根據(jù)ChatMessage響應(yīng)觸發(fā)它們,然后再次返回“agent”節(jié)點(diǎn)。
def should_continue(state: MessagesState):
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls:
return "tools"
return END
def call_model(state: MessagesState):
messages = state["messages"]
response = model.invoke(messages)
return {"messages": [response]}
在每次調(diào)用“agent”節(jié)點(diǎn)(換句話說:基于代碼的代理中的路由器)后,should_concontinue邊決定是將響應(yīng)返回給用戶還是傳遞給ToolNode來處理工具調(diào)用。
在每個(gè)節(jié)點(diǎn)中,“state”存儲(chǔ)來自O(shè)penAI的消息和響應(yīng)列表,這一點(diǎn)類似于基于代碼的代理的方法。
LangGraph方案的挑戰(zhàn)
示例中LangGraph實(shí)現(xiàn)的大部分困難在于LangChain對(duì)象的使用,此方案需要借助這個(gè)對(duì)象來使事情順利進(jìn)行。
挑戰(zhàn)#1:函數(shù)調(diào)用驗(yàn)證
為了使用ToolNode對(duì)象,我必須重構(gòu)我現(xiàn)有的大部分Skill代碼。ToolNode接受一個(gè)可調(diào)用函數(shù)列表,這最初讓我認(rèn)為我可以使用現(xiàn)有的函數(shù),但由于我設(shè)計(jì)的函數(shù)參數(shù)方面的原因,事情發(fā)生了一些變化。
這些技能都被定義為具有可調(diào)用成員函數(shù)的類。這意味著,它們的第一個(gè)參數(shù)是“self”。GPT-4o足夠聰明,不會(huì)在生成的函數(shù)調(diào)用中包含“self”參數(shù),但不幸的是LangGraph將其解讀為由于缺少參數(shù)而導(dǎo)致的驗(yàn)證錯(cuò)誤。
這花了我?guī)讉€(gè)小時(shí)才弄清楚,因?yàn)殄e(cuò)誤消息將函數(shù)中的第三個(gè)參數(shù)(數(shù)據(jù)分析技能上的“args”)標(biāo)記為缺少的參數(shù):
pydantic.v1.error_wrappers.ValidationError: 1 validation error for data_analysis_toolSchema
args field required (type=value_error.missing)
值得一提的是,錯(cuò)誤消息來自Pydantic,而不是LangGraph。
最終,我咬緊牙關(guān),用Langchain的@tool裝飾器將我的技能重新定義為一些基本方法,終于使得代碼正常進(jìn)行。
@tool
def generate_and_run_sql_query(query: str):
"""根據(jù)提示符生成并運(yùn)行一個(gè)SQL查詢。
參數(shù):
query (str): 一個(gè)包含原始用戶提示符的字符串。
返回值:
str: SQL查詢的結(jié)果。
"""
挑戰(zhàn)#2:調(diào)試
正如前文所述,在框架內(nèi)進(jìn)行調(diào)試是頗為困難的事情。這主要?dú)w結(jié)為令人困惑的錯(cuò)誤消息和抽象概念,使查看變量變得更加困難。
抽象概念主要出現(xiàn)在嘗試調(diào)試代理周圍發(fā)送的消息時(shí)。LangGraph將這些消息存儲(chǔ)在狀態(tài)[“messages”]中。圖中的一些節(jié)點(diǎn)會(huì)自動(dòng)從這些消息中提取信息,這可能會(huì)使節(jié)點(diǎn)訪問消息時(shí)難以理解消息的含義。
代理動(dòng)作的順序視圖
LangGraph方案的優(yōu)點(diǎn)
LangGraph的主要優(yōu)點(diǎn)之一是易于使用,因?yàn)閳D形結(jié)構(gòu)代碼干凈且易于訪問。特別是如果你有復(fù)雜的節(jié)點(diǎn)邏輯時(shí),只通過圖的單一視圖有助于更容易地理解代理是如何連接在一起的。LangGraph方案也使得轉(zhuǎn)換現(xiàn)有的基于LangChain構(gòu)建的應(yīng)用程序變得非常簡(jiǎn)單。
小結(jié)
如果你僅使用LangGraph框架中的所有內(nèi)容,那么LangGraph會(huì)順利地工作。但是,如果你還想使用框架外的一些內(nèi)容進(jìn)行開發(fā)的話,那么你需要為調(diào)試一些難題做好準(zhǔn)備。
LlamaIdex工作流
工作流是LLM代理框架領(lǐng)域的新方案,于今年夏天早些時(shí)候首次亮相。與LangGraph一樣,它旨在使循環(huán)代理更容易構(gòu)建。另外,LLM工作流還特別關(guān)注異步運(yùn)行方式。
LLM工作流的一些元素似乎是對(duì)LangGraph的直接響應(yīng),特別是它使用事件而不是邊和條件邊。工作流使用步驟(類似于LangGraph中的節(jié)點(diǎn))來容納邏輯,并且在步驟之間發(fā)送和接收事件。
上面的結(jié)構(gòu)看起來與LangGraph結(jié)構(gòu)相似,只是增加了一點(diǎn)內(nèi)容。我在工作流中添加了一個(gè)設(shè)置步驟來負(fù)責(zé)準(zhǔn)備代理上下文,下面將進(jìn)一步介紹這方面內(nèi)容。值得注意的是,盡管這兩種方案的結(jié)構(gòu)相似,但是驅(qū)動(dòng)它們的代碼卻大不相同。
工作流架構(gòu)
下面的代碼定義了一個(gè)工作流結(jié)構(gòu)。與LangGraph方案中代碼類似,這是我準(zhǔn)備狀態(tài)并將技能附加到LLM對(duì)象的地方。
class AgentFlow(Workflow):
def __init__(self, llm, timeout=300):
super().__init__(timeout=timeout)
self.llm = llm
self.memory = ChatMemoryBuffer(token_limit=1000).from_defaults(llm=llm)
self.tools = []
for func in skill_map.get_function_list():
self.tools.append(
FunctionTool(
skill_map.get_function_callable_by_name(func),
metadata=ToolMetadata(
name=func, description=skill_map.get_function_description_by_name(func)
),
)
)
@step
async def prepare_agent(self, ev: StartEvent) -> RouterInputEvent:
user_input = ev.input
user_msg = ChatMessage(role="user", content=user_input)
self.memory.put(user_msg)
chat_history = self.memory.get()
return RouterInputEvent(input=chat_history)
這也是我定義額外步驟“prepare_agent”的地方。此步驟根據(jù)用戶輸入創(chuàng)建ChatMessage并將其添加到工作流內(nèi)存中。將其拆分為一個(gè)單獨(dú)的步驟意味著,當(dāng)代理循環(huán)執(zhí)行步驟時(shí),我們確實(shí)會(huì)返回它,這樣就避免了將用戶消息重復(fù)添加到內(nèi)存中。
在LangGraph的例子中,我用一個(gè)位于圖外的run_agent方法完成了同樣的事情。這種變化主要是風(fēng)格上的,但在我看來,像我們?cè)谶@里所做的那樣,用工作流和圖形來容納這種邏輯會(huì)更清晰。
設(shè)置好工作流后,我定義了路由代碼:
@step
async def router(self, ev: RouterInputEvent) -> ToolCallEvent | StopEvent:
messages = ev.input
if not any(
isinstance(message, dict) and message.get("role") == "system" for message in messages
):
system_prompt = ChatMessage(role="system", content=SYSTEM_PROMPT)
messages.insert(0, system_prompt)
with using_prompt_template(template=SYSTEM_PROMPT, version="v0.1"):
response = await self.llm.achat_with_tools(
model="gpt-4o",
messages=messages,
tools=self.tools,
)
self.memory.put(response.message)
tool_calls = self.llm.get_tool_calls_from_response(response, error_on_no_tool_call=False)
if tool_calls:
return ToolCallEvent(tool_calls=tool_calls)
else:
return StopEvent(result=response.message.content)
以及工具調(diào)用處理代碼:
@step
async def tool_call_handler(self, ev: ToolCallEvent) -> RouterInputEvent:
tool_calls = ev.tool_calls
for tool_call in tool_calls:
function_name = tool_call.tool_name
arguments = tool_call.tool_kwargs
if "input" in arguments:
arguments["prompt"] = arguments.pop("input")
try:
function_callable = skill_map.get_function_callable_by_name(function_name)
except KeyError:
function_result = "Error: Unknown function call"
function_result = function_callable(arguments)
message = ChatMessage(
role="tool",
content=function_result,
additional_kwargs={"tool_call_id": tool_call.tool_id},
)
self.memory.put(message)
return RouterInputEvent(input=self.memory.get())
上面兩部分代碼看起來都比LangGraph代理更類似于基于代碼的代理。這主要是因?yàn)楣ぷ髁鲗l件路由邏輯保留在步驟中,而不是保留在條件邊中——其中的部分代碼行以前是對(duì)應(yīng)于LangGraph中的條件邊,而現(xiàn)在它們只是路由步驟的一部分——而且LangGraph有一個(gè)ToolNode對(duì)象,它幾乎可以自動(dòng)執(zhí)行tool_call_handler方法中的所有操作。
經(jīng)過路由步驟,我很高興看到的一件事是,我可以將我的SkillMap和基于代碼的代理中的現(xiàn)有技能與工作流一起使用。這些不需要更改即可使用工作流,這讓我的工作變得更加輕松。
工作流程方案的挑戰(zhàn)
挑戰(zhàn)#1:同步與異步
雖然異步執(zhí)行更適合實(shí)時(shí)代理,但調(diào)試同步代理要容易得多。工作流被設(shè)計(jì)為異步工作;因此,試圖強(qiáng)制同步執(zhí)行變得非常困難。
我最初以為我可以刪除“async”方法名稱,并從“achat_with_tools”切換到“chat_with_tools”。然而,由于Workflow類中的底層方法也被標(biāo)記為異步,因此有必要重新定義這些方法以便同步運(yùn)行。我最終堅(jiān)持使用異步方法,但這并沒有使調(diào)試變得更加困難。
代理動(dòng)作的順序視圖
在LangGraph方案的困境重演過程中,圍繞技能上令人困惑的Pydantic驗(yàn)證錯(cuò)誤出現(xiàn)了類似的問題。幸運(yùn)的是,這次這些問題更容易解決,因?yàn)楣ぷ髁髂軌蚝芎玫靥幚沓蓡T函數(shù)。最終,我不得不更加規(guī)范地為我的技能創(chuàng)建LlamaIndex FunctionTool對(duì)象:
for func in skill_map.get_function_list():
self.tools.append(FunctionTool(
skill_map.get_function_callable_by_name(func),
metadata=ToolMetadata(name=func, description=skill_map.get_function_description_by_name(func))))
這段代碼摘自構(gòu)建FunctionTools工具類的函數(shù)AgentFlow__init__。
工作流方案的優(yōu)點(diǎn)
我構(gòu)建工作流代理比構(gòu)建LangGraph代理容易得多,主要是因?yàn)楣ぷ髁魅匀灰笪易约壕帉懧酚蛇壿嫼凸ぞ咛幚泶a,而不是提供內(nèi)置函數(shù)。這也意味著,我的工作流代理看起來與我的基于代碼的代理非常相似。
最大的區(qū)別在于事件的使用。我使用了兩個(gè)自定義事件在代理中的步驟之間移動(dòng):
class ToolCallEvent(Event):
tool_calls: list[ToolSelection]
class RouterInputEvent(Event):
input: list[ChatMessage]
基于事件的“發(fā)射器-接收器”架構(gòu)取代了直接調(diào)用代理中的一些方法,如工具調(diào)用處理程序。
如果你有更復(fù)雜的系統(tǒng),其中有多個(gè)異步觸發(fā)的步驟的話,可能會(huì)發(fā)出多個(gè)事件,那么這種架構(gòu)對(duì)于清晰地管理這些步驟非常有幫助。
工作流的其他好處包括:此方案非常輕量級(jí),不會(huì)給你強(qiáng)加太多的結(jié)構(gòu)(除了使用某些LlamaIdex對(duì)象),而且它基于事件的架構(gòu)為直接函數(shù)調(diào)用提供了一種有用的替代方案,特別是對(duì)于復(fù)雜的異步應(yīng)用程序而言。
框架比較
縱觀上述三種方法,每種方法都各有其優(yōu)點(diǎn)。
無框架方法是最容易實(shí)現(xiàn)的。因?yàn)槿魏纬橄蠖际怯砷_發(fā)人員自己定義的(即上例中的SkillMap對(duì)象),所以保持各種類型和對(duì)象的簡(jiǎn)潔是很容易的。然而,代碼的可讀性和可訪問性完全取決于單個(gè)開發(fā)人員。很容易看出,在沒有采用一些強(qiáng)制的結(jié)構(gòu)定義的情況下,越來越復(fù)雜的代理會(huì)變得一團(tuán)糟。
LangGraph框架本身提供了相當(dāng)多的結(jié)構(gòu),這使得代理的定義非常清晰。如果一個(gè)更大的團(tuán)隊(duì)正在合作開發(fā)一個(gè)代理,這種結(jié)構(gòu)將提供一種有助于實(shí)施架構(gòu)的方法。LangGraph也可能為那些不熟悉結(jié)構(gòu)的人提供一個(gè)很好的代理起點(diǎn)。然而,有一個(gè)權(quán)衡——由于LangGraph為你做了很多工作,如果你不完全接受該框架,可能會(huì)感覺有些頭痛;代碼可能非常干凈,但你可能會(huì)為其付出更多的調(diào)試代價(jià)。
在上述三種方法中,工作流方案位于中間。基于事件的架構(gòu)可能對(duì)某些項(xiàng)目非常有幫助;事實(shí)上,在使用LlamaIdex類型方面所需的編碼量更少,這為那些在應(yīng)用程序中沒有完全使用過該框架的人提供了更大的靈活性。
最終,核心問題可能歸結(jié)為“你已經(jīng)在使用LlamaIndex或LangChain來編排你的應(yīng)用程序了嗎?”LangGraph和工作流都與各自的底層框架緊密相連,以至于每個(gè)特定于代理的框架的額外好處可能不會(huì)讓你只憑優(yōu)點(diǎn)來切換它們。
不過,純代碼方法可能永遠(yuǎn)是一個(gè)具有吸引力的選擇。如果你有足夠的嚴(yán)謹(jǐn)性來記錄和執(zhí)行任何創(chuàng)建的抽象的話,那么就很容易確保外部框架中沒有任何設(shè)置會(huì)減緩你的開發(fā)速度。
選擇代理框架的關(guān)鍵問題
當(dāng)然,“視情況而定”從來不是一個(gè)令人滿意的答案。下面的三個(gè)問題可以幫助你決定在下一個(gè)代理項(xiàng)目中使用哪個(gè)框架。
- 你是否已經(jīng)在項(xiàng)目的重要部分使用LlamaIndex或LangChain?
如果是,請(qǐng)先優(yōu)先分析這種選擇方案。
- 你熟悉常見的代理結(jié)構(gòu)嗎?還是想知道你應(yīng)該如何構(gòu)建代理?
如果你大致屬于后一種情形,請(qǐng)嘗試工作流方案。不過,如果你真的屬于后一種情形,試試LangGraph方案吧。
- 以前構(gòu)建過你自己的代理嗎?
框架方案的好處之一是,每個(gè)框架都有許多教程和示例。相比而言,可用于構(gòu)建純代碼代理的示例代碼要少得多。
結(jié)論
無論如何,選擇一個(gè)代理框架只是影響生成式人工智能系統(tǒng)生產(chǎn)結(jié)果的眾多選擇之一。與往常一樣,構(gòu)建強(qiáng)大的保護(hù)措施并進(jìn)行LLM跟蹤是非常值得推薦的做法,并且隨著新的代理框架、研究成果和模型不斷顛覆既定技術(shù),這樣做也變得更為機(jī)動(dòng)靈活。
譯者介紹
朱先忠,51CTO社區(qū)編輯,51CTO專家博客、講師,濰坊一所高校計(jì)算機(jī)教師,自由編程界老兵一枚。
原文標(biāo)題:Choosing Between LLM Agent Frameworks,作者:Aparna Dhinakaran