到目前为止,我们已经讨论了 LLM 应用的最常见特性:
- 引导技术(在前言和第一章中)
- RAG(在第二章和第三章中)
- 内存(在第四章中)
接下来的问题应该是:我们如何将这些部分组装成一个连贯的应用,以实现我们设定的目标?为了类比建筑领域,游泳池和一层楼房虽然都是由相同的材料建成的,但显然它们有着非常不同的用途。使它们适合各自不同用途的,是如何将这些材料结合的计划——即它们的架构。构建 LLM 应用程序时也是如此。你必须做出的最重要的决策是如何将你所拥有的不同组件(如 RAG、引导技术、内存)组合成一个能实现你目的的系统。
在我们讨论具体架构之前,先通过一个例子来说明。任何你可能构建的 LLM 应用都将从一个目的开始:应用的设计目标。假设你想要构建一个电子邮件助手——一个在你查看电子邮件之前先读取电子邮件并减少你需要查看的邮件数量的 LLM 应用。这个应用可能通过归档一些不感兴趣的邮件、直接回复一些邮件,并将其他邮件标记为稍后需要关注的邮件来实现这一点。
你可能还希望这个应用在执行任务时受到一些约束。列出这些约束非常重要,因为它们将帮助指导你寻找合适的架构。第八章将更详细地介绍这些约束以及如何处理它们。对于这个假设的电子邮件助手,假设我们希望它能做到以下几点:
- 最小化它打扰你的次数(毕竟,应用的主要目的是节省时间)。
- 避免让你的电子邮件通讯对象收到你自己永远不会发出的回复。
这暗示了构建 LLM 应用时经常面临的关键权衡:自主性(或自主行动的能力)与可靠性(或你可以信任其输出的程度)之间的权衡。直观地说,如果电子邮件助手在没有你干预的情况下做更多的操作,它将更有用,但如果过度依赖它,它不可避免地会发送你希望它不要发送的邮件。
衡量 LLM 应用自主性的一个方式是评估应用行为的多少由 LLM 决定(与代码相比):
- 让 LLM 决定某一步的输出(例如,写一封电子邮件的草稿回复)。
- 让 LLM 决定下一步要做什么(例如,对于一封新邮件,决定在归档、回复或标记为待审三种操作中选择哪一个)。
- 让 LLM 决定可以采取哪些步骤(例如,让 LLM 编写代码,执行你没有预编程进应用中的动态操作)。
我们可以根据 LLM 应用中不同的自主性程度来分类一些流行的构建 LLM 应用的方案,即,哪些任务由 LLM 处理,哪些仍由开发者或用户掌控。这些方案可以称为认知架构。在人工智能领域,认知架构这个术语长期以来用于指代人类推理的模型(以及它们在计算机中的实现)。我们所知,LLM 认知架构这个术语首次应用于 LLM 的研究论文中。LLM 认知架构可以定义为构建 LLM 应用的步骤方案(参见图 5-1)。一个步骤,例如,可能是检索相关文档(RAG)或使用链式思维提示调用 LLM。
现在让我们看看在构建应用程序时可以使用的每个主要架构或方案(如图 5-1 所示):
0: 代码
这不是一个 LLM 认知架构(因此我们标号为 0),因为它根本不使用 LLM。你可以将其视为你通常编写的常规软件。对于本书而言,真正有趣的架构是下一个。
1: LLM 调用
这是到目前为止我们在书中看到的大多数示例,只有一次 LLM 调用。它主要在作为更大应用的一部分时使用,该应用利用 LLM 来完成特定任务,例如翻译或总结一段文本。
2: 链式调用
可以说,下一步是使用多个 LLM 调用,按照预定义的顺序。例如,一个文本到 SQL 的应用(它接收用户提供的关于如何对数据库进行某个计算的自然语言描述作为输入)可能会使用两个按顺序调用的 LLM:
- 第一次 LLM 调用将用户提供的自然语言查询和开发者提供的数据库内容描述转化为 SQL 查询。
- 第二次 LLM 调用生成一个查询的解释,适合非技术用户理解,给定上一个调用生成的查询。这个可以用来帮助用户检查生成的查询是否符合他们的要求。
3: 路由器
下一个步骤是使用 LLM 来定义要执行的步骤序列。也就是说,链式架构总是执行开发者确定的静态步骤序列(无论多少步骤),而路由器架构的特点是使用 LLM 在一些预定义的步骤之间进行选择。一个例子是一个 RAG 应用,它有多个来自不同领域的文档索引,流程如下:
- 第一次 LLM 调用根据用户提供的查询和开发者提供的索引描述选择使用哪个索引。
- 一个检索步骤,查询选择的索引以获取与用户查询最相关的文档。
- 第二次 LLM 调用,在用户提供的查询和从索引中获取的相关文档列表的基础上生成答案。
这就是本章的内容。我们将依次讨论这些架构。接下来的章节将讨论代理架构,它们更广泛地使用 LLM。但首先,让我们先讨论一些更好的工具来帮助我们完成这段旅程。
架构 #1:LLM 调用
作为 LLM 调用架构的示例,我们将回到第四章中创建的聊天机器人。这个聊天机器人将直接回应用户的消息。
首先,创建一个 StateGraph,并向其中添加一个节点来表示 LLM 调用:
Python:
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
model = ChatOpenAI()
class State(TypedDict):
# 消息的类型是 "list"。注解中的 `add_messages`
# 函数定义了该状态应该如何更新(在此情况下,它将新消息附加到
# 列表中,而不是替换先前的消息)
messages: Annotated[list, add_messages]
def chatbot(state: State):
answer = model.invoke(state["messages"])
return {"messages": [answer]}
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, 'chatbot')
builder.add_edge('chatbot', END)
graph = builder.compile()
JavaScript:
import {
StateGraph,
Annotation,
messagesStateReducer,
START, END
} from '@langchain/langgraph'
import {ChatOpenAI} from '@langchain/openai'
const model = new ChatOpenAI()
const State = {
/**
* State 定义了三件事:
* 1. 图状态的结构(哪些 "渠道" 可用于读写)
* 2. 状态渠道的默认值
* 3. 状态渠道的 reducers。Reducers 是决定如何将更新应用于状态的函数。
* 在下面,新的消息将附加到消息数组中。
*/
messages: Annotation({
reducer: messagesStateReducer,
default: () => []
}),
}
async function chatbot(state) {
const answer = await model.invoke(state.messages)
return {"messages": answer}
}
const builder = new StateGraph(State)
.addNode('chatbot', chatbot)
.addEdge(START, 'chatbot')
.addEdge('chatbot', END)
const graph = builder.compile()
我们还可以绘制图的可视化表示:
Python:
graph.get_graph().draw_mermaid_png()
JavaScript:
await graph.getGraph().drawMermaidPng()
我们刚刚创建的图形如图 5-2 所示。
你可以使用之前章节中看到的熟悉的 stream() 方法运行它:
Python:
input = {"messages": [HumanMessage('hi!')]}
for chunk in graph.stream(input):
print(chunk)
JavaScript:
const input = {messages: [new HumanMessage('hi!')]}
for await (const chunk of await graph.stream(input)) {
console.log(chunk)
}
输出:
{ "chatbot": { "messages": [AIMessage("How can I help you?")] } }
注意,传递给图的输入与我们之前定义的 State 对象的形状相同;也就是说,我们通过字典中的 messages 键发送了一条消息列表。
这是使用 LLM 的最简单架构,并不是说它不应该被使用。以下是一些你可能会在流行产品中看到它应用的例子,当然还有很多其他场景:
- 由 AI 提供支持的功能,如总结和翻译(例如,你可以在流行的写作软件 Notion 中找到),可以通过一次 LLM 调用来实现。
- 简单的 SQL 查询生成也可以通过一次 LLM 调用来实现,这取决于开发者设想的用户体验和目标用户。
架构 #2:链式调用
这个架构通过使用多个 LLM 调用,按预定义的顺序扩展了前面的内容(也就是说,不同的应用调用执行相同的 LLM 调用序列,尽管输入和结果不同)。
让我们以一个文本到 SQL 的应用为例,它接收用户输入的自然语言描述,用于对数据库进行某些计算。我们之前提到过,这可以通过一次 LLM 调用来生成 SQL 查询,但我们可以通过使用多个 LLM 调用按顺序创建一个更复杂的应用。有些作者称这种架构为流工程(flow engineering)。
首先,我们用文字描述一下流程:
- 第一次 LLM 调用将用户提供的自然语言查询和开发者提供的数据库内容描述转化为 SQL 查询。
- 第二次 LLM 调用根据上一步生成的查询,编写一个适合非技术用户理解的查询解释。这个解释可以用来帮助用户检查生成的查询是否符合他们的要求。
你也可以进一步扩展这个应用(不过我们在这里不做讨论),加入更多的步骤:
- 执行查询,查询数据库并返回二维表。
- 使用第三次 LLM 调用,将查询结果总结为原始用户问题的文本答案。
现在,让我们用 LangGraph 实现这个过程:
Python:
from typing import Annotated, TypedDict
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
# 用于生成 SQL 查询
model_low_temp = ChatOpenAI(temperature=0.1)
# 用于生成自然语言输出
model_high_temp = ChatOpenAI(temperature=0.7)
class State(TypedDict):
# 跟踪对话历史
messages: Annotated[list, add_messages]
# 输入
user_query: str
# 输出
sql_query: str
sql_explanation: str
class Input(TypedDict):
user_query: str
class Output(TypedDict):
sql_query: str
sql_explanation: str
generate_prompt = SystemMessage(
"""You are a helpful data analyst who generates SQL queries for users based
on their questions."""
)
def generate_sql(state: State) -> State:
user_message = HumanMessage(state["user_query"])
messages = [generate_prompt, *state["messages"], user_message]
res = model_low_temp.invoke(messages)
return {
"sql_query": res.content,
# 更新对话历史
"messages": [user_message, res],
}
explain_prompt = SystemMessage(
"You are a helpful data analyst who explains SQL queries to users."
)
def explain_sql(state: State) -> State:
messages = [
explain_prompt,
# 包含用户的查询和上一步的 SQL 查询
*state["messages"],
]
res = model_high_temp.invoke(messages)
return {
"sql_explanation": res.content,
# 更新对话历史
"messages": res,
}
builder = StateGraph(State, input=Input, output=Output)
builder.add_node("generate_sql", generate_sql)
builder.add_node("explain_sql", explain_sql)
builder.add_edge(START, "generate_sql")
builder.add_edge("generate_sql", "explain_sql")
builder.add_edge("explain_sql", END)
graph = builder.compile()
JavaScript:
import {
HumanMessage,
SystemMessage
} from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import {
StateGraph,
Annotation,
messagesStateReducer,
START,
END,
} from "@langchain/langgraph";
// 用于生成 SQL 查询
const modelLowTemp = new ChatOpenAI({ temperature: 0.1 });
// 用于生成自然语言输出
const modelHighTemp = new ChatOpenAI({ temperature: 0.7 });
const annotation = Annotation.Root({
messages: Annotation({ reducer: messagesStateReducer, default: () => [] }),
user_query: Annotation(),
sql_query: Annotation(),
sql_explanation: Annotation(),
});
const generatePrompt = new SystemMessage(
`You are a helpful data analyst who generates SQL queries for users based on
their questions.`
);
async function generateSql(state) {
const userMessage = new HumanMessage(state.user_query);
const messages = [generatePrompt, ...state.messages, userMessage];
const res = await modelLowTemp.invoke(messages);
return {
sql_query: res.content as string,
// 更新对话历史
messages: [userMessage, res],
};
}
const explainPrompt = new SystemMessage(
"You are a helpful data analyst who explains SQL queries to users."
);
async function explainSql(state) {
const messages = [explainPrompt, ...state.messages];
const res = await modelHighTemp.invoke(messages);
return {
sql_explanation: res.content as string,
// 更新对话历史
messages: res,
};
}
const builder = new StateGraph(annotation)
.addNode("generate_sql", generateSql)
.addNode("explain_sql", explainSql)
.addEdge(START, "generate_sql")
.addEdge("generate_sql", "explain_sql")
.addEdge("explain_sql", END);
const graph = builder.compile();
这个图的可视化表示见图 5-3。
这是一个输入和输出的示例:
Python:
graph.invoke({
"user_query": "What is the total sales for each product?"
})
JavaScript:
await graph.invoke({
user_query: "What is the total sales for each product?"
})
输出:
{
"sql_query": "SELECT product_name, SUM(sales_amount) AS total_sales\nFROM
sales\nGROUP BY product_name;",
"sql_explanation": "This query will retrieve the total sales for each product
by summing up the sales_amount column for each product and grouping the
results by product_name."
}
首先,执行 generate_sql 节点,它会填充状态中的 sql_query 键(这将成为最终输出的一部分),并使用新消息更新 messages 键。然后,执行 explain_sql 节点,使用前一步生成的 SQL 查询,并填充状态中的 sql_explanation 键。此时,图的运行完成,输出将返回给调用者。
还需要注意的是,在创建 StateGraph 时使用了独立的输入和输出模式。这使得你可以定制哪些部分的状态作为用户输入被接受,哪些作为最终输出返回给用户。其余的状态键由图节点在内部使用,用于保持中间状态,并作为 stream() 生成的流式输出的一部分提供给用户。
架构 #3:路由器
这个架构通过将接下来的责任分配给 LLM,进一步提升了自主性:决定下一步要采取的行动。也就是说,虽然链式架构总是执行静态的步骤序列(无论多少步骤),但路由器架构的特点是使用 LLM 在某些预定义步骤之间进行选择。
让我们以一个 RAG 应用为例,该应用访问多个来自不同领域的文档索引(参考第二章了解更多关于索引的内容)。通常,你可以通过避免将不相关的信息包含在提示中来提高 LLM 的性能。因此,在构建这个应用时,我们应该尝试为每个查询选择正确的索引,并仅使用该索引。这个架构中的关键发展是使用 LLM 来做出这个决定,实际上是通过 LLM 来评估每个传入的查询,并决定对于该查询应使用哪个索引。
注意:
在 LLM 出现之前,通常解决这个问题的方法是使用机器学习技术和一个数据集,通过将用户查询映射到正确的索引来构建一个分类器模型。这可能会非常具有挑战性,因为它需要以下步骤:
- 手动组装该数据集
- 为每个用户查询生成足够的特征(定量属性),以便训练分类器
而 LLM 由于其对人类语言的编码能力,可以在几乎没有任何示例或额外训练的情况下有效地作为这个分类器。
首先,我们用文字描述一下流程:
- 一次 LLM 调用,根据用户提供的查询和开发者提供的索引描述,选择使用哪个索引。
- 一个检索步骤,查询选定的索引以获取与用户查询最相关的文档。
- 另一次 LLM 调用,在用户提供的查询和从索引中获取的相关文档列表的基础上生成答案。
现在,让我们用 LangGraph 实现它:
Python:
from typing import Annotated, Literal, TypedDict
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.vectorstores.in_memory import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
embeddings = OpenAIEmbeddings()
# 用于生成 SQL 查询
model_low_temp = ChatOpenAI(temperature=0.1)
# 用于生成自然语言输出
model_high_temp = ChatOpenAI(temperature=0.7)
class State(TypedDict):
# 跟踪对话历史
messages: Annotated[list, add_messages]
# 输入
user_query: str
# 输出
domain: Literal["records", "insurance"]
documents: list[Document]
answer: str
class Input(TypedDict):
user_query: str
class Output(TypedDict):
documents: list[Document]
answer: str
# 参考第2章,了解如何将文档填充到向量存储中
medical_records_store = InMemoryVectorStore.from_documents([], embeddings)
medical_records_retriever = medical_records_store.as_retriever()
insurance_faqs_store = InMemoryVectorStore.from_documents([], embeddings)
insurance_faqs_retriever = insurance_faqs_store.as_retriever()
router_prompt = SystemMessage(
"""You need to decide which domain to route the user query to. You have two
domains to choose from:
- records: contains medical records of the patient, such as
diagnosis, treatment, and prescriptions.
- insurance: contains frequently asked questions about insurance
policies, claims, and coverage.
Output only the domain name."""
)
def router_node(state: State) -> State:
user_message = HumanMessage(state["user_query"])
messages = [router_prompt, *state["messages"], user_message]
res = model_low_temp.invoke(messages)
return {
"domain": res.content,
# 更新对话历史
"messages": [user_message, res],
}
def pick_retriever(
state: State,
) -> Literal["retrieve_medical_records", "retrieve_insurance_faqs"]:
if state["domain"] == "records":
return "retrieve_medical_records"
else:
return "retrieve_insurance_faqs"
def retrieve_medical_records(state: State) -> State:
documents = medical_records_retriever.invoke(state["user_query"])
return {
"documents": documents,
}
def retrieve_insurance_faqs(state: State) -> State:
documents = insurance_faqs_retriever.invoke(state["user_query"])
return {
"documents": documents,
}
medical_records_prompt = SystemMessage(
"""You are a helpful medical chatbot who answers questions based on the
patient's medical records, such as diagnosis, treatment, and
prescriptions."""
)
insurance_faqs_prompt = SystemMessage(
"""You are a helpful medical insurance chatbot who answers frequently asked
questions about insurance policies, claims, and coverage."""
)
def generate_answer(state: State) -> State:
if state["domain"] == "records":
prompt = medical_records_prompt
else:
prompt = insurance_faqs_prompt
messages = [
prompt,
*state["messages"],
HumanMessage(f"Documents: {state['documents']}"),
]
res = model_high_temp.invoke(messages)
return {
"answer": res.content,
# 更新对话历史
"messages": res,
}
builder = StateGraph(State, input=Input, output=Output)
builder.add_node("router", router_node)
builder.add_node("retrieve_medical_records", retrieve_medical_records)
builder.add_node("retrieve_insurance_faqs", retrieve_insurance_faqs)
builder.add_node("generate_answer", generate_answer)
builder.add_edge(START, "router")
builder.add_conditional_edges("router", pick_retriever)
builder.add_edge("retrieve_medical_records", "generate_answer")
builder.add_edge("retrieve_insurance_faqs", "generate_answer")
builder.add_edge("generate_answer", END)
graph = builder.compile()
JavaScript:
import {
HumanMessage,
SystemMessage
} from "@langchain/core/messages";
import {
ChatOpenAI,
OpenAIEmbeddings
} from "@langchain/openai";
import {
MemoryVectorStore
} from "langchain/vectorstores/memory";
import {
DocumentInterface
} from "@langchain/core/documents";
import {
StateGraph,
Annotation,
messagesStateReducer,
START,
END,
} from "@langchain/langgraph";
const embeddings = new OpenAIEmbeddings();
// 用于生成 SQL 查询
const modelLowTemp = new ChatOpenAI({ temperature: 0.1 });
// 用于生成自然语言输出
const modelHighTemp = new ChatOpenAI({ temperature: 0.7 });
const annotation = Annotation.Root({
messages: Annotation({ reducer: messagesStateReducer, default: () => [] }),
user_query: Annotation(),
domain: Annotation(),
documents: Annotation(),
answer: Annotation(),
});
// 参考第2章,了解如何将文档填充到向量存储中
const medicalRecordsStore = await MemoryVectorStore.fromDocuments(
[],
embeddings
);
const medicalRecordsRetriever = medicalRecordsStore.asRetriever();
const insuranceFaqsStore = await MemoryVectorStore.fromDocuments(
[],
embeddings
);
const insuranceFaqsRetriever = insuranceFaqsStore.asRetriever();
const routerPrompt = new SystemMessage(
`You need to decide which domain to route the user query to. You have two
domains to choose from:
- records: contains medical records of the patient, such as diagnosis,
treatment, and prescriptions.
- insurance: contains frequently asked questions about insurance
policies, claims, and coverage.
Output only the domain name.`
);
async function routerNode(state) {
const userMessage = new HumanMessage(state.user_query);
const messages = [routerPrompt, ...state.messages, userMessage];
const res = await modelLowTemp.invoke(messages);
return {
domain: res.content as "records" | "insurance",
// 更新对话历史
messages: [userMessage, res],
};
}
function pickRetriever(state) {
if (state.domain === "records") {
return "retrieve_medical_records";
} else {
return "retrieve_insurance_faqs";
}
}
async function retrieveMedicalRecords(state) {
const documents = await medicalRecordsRetriever.invoke(state.user_query);
return {
documents,
};
}
async function retrieveInsuranceFaqs(state) {
const documents = await insuranceFaqsRetriever.invoke(state.user_query);
return {
documents,
};
}
const medicalRecordsPrompt = new SystemMessage(
`You are a helpful medical chatbot who answers questions based on the
patient's medical records, such as diagnosis, treatment, and
prescriptions.`
);
const insuranceFaqsPrompt = new SystemMessage(
`You are a helpful medical insurance chatbot who answers frequently asked
questions about insurance policies, claims, and coverage.`
);
async function generateAnswer(state) {
const prompt =
state.domain === "records" ? medicalRecordsPrompt : insuranceFaqsPrompt;
const messages = [
prompt,
...state.messages,
new HumanMessage(`Documents: ${state.documents}`),
];
const res = await modelHighTemp.invoke(messages);
return {
answer: res.content as string,
// 更新对话历史
messages: res,
};
}
const builder = new StateGraph(annotation)
.addNode("router", routerNode)
.addNode("retrieve_medical_records", retrieveMedicalRecords)
.addNode("retrieve_insurance_faqs", retrieveInsuranceFaqs)
.addNode("generate_answer", generateAnswer)
.addEdge(START, "router")
.addConditionalEdges("router", pickRetriever)
.addEdge("retrieve_medical_records", "generate_answer")
.addEdge("retrieve_insurance_faqs", "generate_answer")
.addEdge("generate_answer", END);
const graph = builder.compile();
图形的可视化表示如图 5-4 所示。
注意,这开始变得更加有用,因为它展示了通过图的两条可能路径:通过 retrieve_medical_records 或通过 retrieve_insurance_faqs,而且对于这两条路径,我们首先访问 router 节点,最后访问 generate_answer 节点。这两条可能的路径是通过使用条件边实现的,这个条件边在 pick_retriever 函数中实现,将 LLM 选择的域映射到前面提到的两个节点之一。条件边在图 5-4 中作为从源节点到目标节点的虚线表示。
现在来看一下示例输入和输出,这次是带有流式输出的:
Python:
input = {
"user_query": "Am I covered for COVID-19 treatment?"
}
for c in graph.stream(input):
print(c)
JavaScript:
const input = {
user_query: "Am I covered for COVID-19 treatment?"
}
for await (const chunk of await graph.stream(input)) {
console.log(chunk)
}
输出(实际答案没有显示,因为它将取决于你的文档):
{
"router": {
"messages": [
HumanMessage(content="Am I covered for COVID-19 treatment?"),
AIMessage(content="insurance")
],
"domain": "insurance"
}
}
{
"retrieve_insurance_faqs": {
"documents": [...]
}
}
{
"generate_answer": {
"messages": AIMessage(
content="...",
),
"answer": "..."
}
}
这个输出流包含了在本次执行图时,每个节点返回的值。我们逐一解释这些内容。每个字典中的顶层键是节点的名称,且该键的值是该节点返回的内容:
router节点返回了对messages的更新(这使我们能够轻松地使用前面描述的内存技术继续这次对话),以及 LLM 为这个用户查询选择的域,在此例中是insurance。- 然后
pick_retriever函数运行,并根据前一步 LLM 调用识别的域返回了下一个要运行的节点名称。 - 接着,
retrieve_insurance_faqs节点运行,返回了来自该索引的一组相关文档。这意味着在之前绘制的图中,我们选择了 LLM 决定的左路径。 - 最后,
generate_answer节点运行,它使用这些文档和原始用户查询生成了一个答案,并将其写入状态中(同时更新了messages键)。
总结
本章讨论了构建 LLM 应用时的关键权衡:自主性与监督性。LLM 应用的自主性越高,它能够做的事情就越多——但这也提高了对其行动进行控制的需求。接着,我们介绍了不同的认知架构,它们在自主性和监督性之间达到了不同的平衡。
第六章将讨论我们迄今为止见过的最强大的认知架构:代理架构。
- Theodore R. Sumers 等人,“Cognitive Architectures for Language Agents”,arXiv,2023年9月5日,更新于2024年3月15日。
- Tal Ridnik 等人,“Code Generation with AlphaCodium: From Prompt Engineering to Flow Engineering”,arXiv,2024年1月16日。