AI 大模型:从零搭建 Agent 框架
AI Agent 是基于语言模型的自主智能实体,通过规划、记忆和工具调用实现复杂任务。文章介绍了 Agent 的核心组件及思维链(CoT)、ReAct 等推理框架。详细阐述了如何从零搭建 ReAct 框架,包括 Agent 输入输出处理、工具定义与函数声明生成、以及执行器循环逻辑。通过 TypeScript 代码示例展示了 AgentExecutor 的运行机制,并指出了后续可优化的方向如错误处理和日志记录。

AI Agent 是基于语言模型的自主智能实体,通过规划、记忆和工具调用实现复杂任务。文章介绍了 Agent 的核心组件及思维链(CoT)、ReAct 等推理框架。详细阐述了如何从零搭建 ReAct 框架,包括 Agent 输入输出处理、工具定义与函数声明生成、以及执行器循环逻辑。通过 TypeScript 代码示例展示了 AgentExecutor 的运行机制,并指出了后续可优化的方向如错误处理和日志记录。

Agent 的核心思想是使用语言模型来选择要采取的一系列操作。在 Agent 中,语言模型被用作推理引擎来确定要采取哪些操作以及按什么顺序。相比于传统机械或软件被动的'给予输入——>做出输出'的模式,Agent 由于更加强调自主的发现问题、确定目标、构想方案、选择方案、执行方案、检查更新的特性,因此可以被认为是一类拥有'自主智能的实体',而被广泛称之为智能体。
下文介绍的 Plan/Memory/Tool 三组件只是 Agent 框架一种简单的拆分(参考了 LangChain),除此外还有多种理解 Agent 框架的视角,读者若感兴趣可以自行学习以下 Agent 框架:
Agent 学习调用外部 API 来获取模型权重中缺失的额外信息,通常这些信息在预训练后很难更改,包括当前信息、代码执行能力、对专有信息源的访问等。 工具是代理可以调用的功能,本质上就是一个函数。使用工具是 AI Agent 最迷人最先进的特性。
语言智能可以被理解为'使用基于自然语言的概念对经验事物进行**'理解'以及在概念之间进行'推理'**的能力'。
理解能力上,作为'语言模型'的大模型具备概念理解能力并不难理解,但是仅仅像 Word2vec 一样只能得到'国王'与'男人'的'距离'更近的结论对于语言智能而言必然远远不够。
真正让人惊讶的是大模型在推理上的能力涌现。推理,一般指根据几个已知的前提推导得出新的结论的过程,区别于理解,推理一般是一个'多步骤'的过程,推理的过程可以形成非常必要的'中间概念',这些中间概念将辅助复杂问题的求解。
2022 年,在 Google 发布的论文《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》中首次提出,通过让大模型逐步参与将一个复杂问题分解为一步一步的子问题并依次进行求解的过程可以显著提升大模型的性能。而这一系列推理的中间步骤就被称为思维链(Chain of Thought)。
CoT 大家应该都比较熟了,使用它的方法很简单:
对于 Zero-Shot,只需要在 Prompt 的结尾加一句:
Let's think step by step
对于 One-Shot 或 Few-Shot,需要在 Prompt 中适当地为大模型提供一些示例。
拓展:CoT 其实也在逐渐进化,出现了各种让大模型进行复杂链路思考的方式。
无论是环境的反馈,还是人类的指令,Agent 都需要完成一个对接收到的信息进行'理解',并依据得到的理解进行意图识别,转化为下一步任务的过程。在前文中之所以介绍 CoT,是因为使用 CoT 可以大大帮助模型对现有输入进行'感知',激活大模型对任务的拆分规划和推理能力。借鉴 CoT,我们可以归纳出基本的 Agent 框架并延伸,ReAct 就是其中之一。
从引入了一个框架,其中 LLMs 以交错的方式生成 推理轨迹 和 任务特定操作。
生成推理轨迹使模型能够诱导、跟踪和更新操作计划,甚至处理异常情况。操作步骤允许与外部源(如知识库或环境)进行交互并且收集信息。
ReAct 框架允许 LLMs 与外部工具交互来获取额外信息,从而给出更可靠和实际的回应。
结果表明,ReAct 可以在语言和决策任务上的表现要高于几个最先进水准要求的基线。ReAct 还提高了 LLMs 的人类可解释性和可信度。总的来说,作者发现了将 ReAct 和链式思考 (CoT) 结合使用的最好方法是在推理过程同时使用内部知识和获取到的外部信息。
ReAct 的灵感来自于'行为'和'推理'之间的协同作用,正是这种协同作用使得人类能够学习新任务并做出决策或推理。
链式思考 (CoT) 提示显示了 LLMs 执行推理轨迹以生成涉及算术和常识推理的问题的答案的能力,以及其他任务。但它因缺乏和外部世界的接触或无法更新自己的知识,而导致事实幻觉和错误传播等问题。
ReAct 是一个将推理和行为与 LLMs 相结合通用的范例。ReAct 提示 LLMs 为任务生成口头推理轨迹和操作。这使得系统执行动态推理来创建、维护和调整操作计划,同时还支持与外部环境 (例如,Wikipedia) 的交互,以将额外信息合并到推理中。
常用模板:
Answer the following questions as best you can. You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {input}
Thought:{agent_scratchpad}
先对各组件进行硬编码。我们为大模型提供一个 get_word_length 工具(Tool),使用 ReAct 框架看看他能否解决一些基本问题。
System Prompt:
Answer the following questions as best you can. You have access to the following tools:
get_word_length(word: str) -> int:
"""Returns the length of a word."""
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_word_length]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
User Input:
Question: How many letters in the word educa
注意!这里只是简单在用大模型调试 prompt,get_word_length 工具并没有具体的代码实现也没有运行。之所以能算出结果都是靠大模型自身的推理能力!
Agent 相当于整体框架的思维推理系统,通常由大模型、Prompt 提供支持。不同的智能体有不同的推理提示风格、不同的输入方式以及不同的解析输出方式,依赖于用户对应用的自定义,说白了就是对大模型进行一层封装更方便管理。
这里提到的 Agent 其实就是大模型本身,我们在框架中最好将大模型的 API 进行面向对封装,方便与框架中其他组件交互。
这里我使用的是 OpenAI 的 GPT-3.5-turbo 模型。
class LLMSingleActionAgent {
llm: AzureLLM
tools: StructuredTool[]
stop: string[]
private _prompt: string = '{input}'
constructor({ llm, tools = [], stop = [] }: LLMSingleActionAgentParams) {
this.llm = llm
this.tools = tools
if (stop.length > 4)
throw new Error('up to 4 stop sequences')
this.stop = stop
}
}
对大模型的输入可以是普通的 Prompt 字符串;也可以是键值对,结合 Prompt Template 拼接出最终的 Prompt 字符串。
输出是要执行的下一个操作或要发送给用户的最终响应(AgentAction 或 AgentFinish)。
对大模型的输入可以是普通的 Prompt 字符串;也可以是键值对,结合 Prompt Template 拼接出最终的 Prompt 字符串。我们这里使用 ReAct 框架,每次输入的 Prompt 都有固定的模板 Template(见上文 ReAct 的模板)。因此需要一个填充模板的函数,我们简单约定变量按 {var} 的格式插入,用正则表达式将字符串替换。
function fillPromptTemplate(promptTemplate: string, inputs: promptInputs) {
let res = promptTemplate
for (const [key, val] of Object.entries(inputs))
res = res.replaceAll(new RegExp(`{\s*${key}\s*}`, 'g'), val)
return res
}
这是一个数据类,表示代理应采取的操作。它有一个 tool 属性(这是应该调用的工具的名称)和一个 tool_input 属性(该工具的输入)。
这表示代理准备好返回给用户时的最终结果。它包含一个 return_values 键值映射,其中包含最终的代理输出。通常,这包含 output 键,其中包含代理响应的字符串。
这些代表先前的代理操作以及当前代理运行的相应输出。这些对于传递到未来的迭代非常重要,因此代理知道它已经完成了哪些工作。其类型为 List[Tuple[AgentAction, Any]]。请注意,观察目前保留为 Any 类型,以实现最大程度的灵活性。实际上,这通常是一个字符串。
export abstract class StructuredTool {
name: string
description: string
constructor(name: string, description: string) {
this.name = name
this.description = description
}
abstract call(arg: string, config?: Record<string, any>): Promise<string>
getSchema(): string {
return `${this.declaration} | ${this.name} | ${this.description}`
}
abstract get declaration(): string
}
工具类有 name 和 description 两个属性,通过 getSchema 函数返回对该工具的文本描述。这里我们先简单地将两个描述信息拼接一下,为 Agent 提供 4 个算数工具。
注意!这里只是简单在用大模型调试 prompt,这几个工具并没有具体的代码实现也没有运行。之所以能算出结果都是靠大模型自身的推理能力!(简单的加减乘除即使没有外部工具他也能做到)
1. Addition Tool: A tool for adding two numbers
2. Subtraction Tool: A tool for subtracting two numbers
3. Division Tool: A tool for dividing two numbers
4. Multiplication Tool: A tool for multiplying two numbers
简单地运行一个应用算术题:
尽管 Agent 经过多步思考成功解决了这个问题,但有个小 bug,在第二步中,Agent 调用乘法工具传入了 3 个参数。如果我们实现工具的时候函数只接受两个参数这里就会报错。
出现这个问题是因为我们提供的工具描述还不够准确,不仅要提供工具的名字和功能描述,最好的就是将函数的声明也带上。因为我们的函数代码本身是硬编码的,要尽一切可能降低大模型输出的随机性,严格按我们的要求输出。
一个笨办法就是直接 copy 自己写的函数声明,硬编码到工具的 description 中。
按两下 cv 键还是很快的。缺点就是 1. 不够灵活,修改函数声明时需要把硬编码的字符串也更改 2. 万一写的是宽松类型的代码(js、python),还得自己想函数声明。
例如对于 call 函数,直接把这个复制下来放到 description 中就行了。
根据'大模型不收敛'定理——当你想为大模型做一件事时,先想想这件事本身是不是也能让大模型做。我们也可以让大模型为工具函数生成函数声明。
使用下面的 prompt:
请为下面的{language}代码生成函数声明:
{code}
同理也可以帮助生成函数功能描述。
请用一两句话描述下面{language}代码的功能:
{code}
该方案的优缺点和方案一相同。主要针对宽松类型的代码,省了点脑子。
Zod 是一个以 TypeScript 为首的模式声明和验证库,弥补了 TypeScript 无法在运行时进行校验的问题。
依靠 zod 的一些插件,我们可以直接将 zod 定义的类型对象转换成类型声明字符串。
例如:
根据自己应用联调的需求选择即可。
优点就是非常灵活,且支持运行时。唯一的缺点就是要学习 zod 的用法。
将函数声明加入 Prompt 后,可以看到 Agent 学会了多次进行乘法,符合我们的函数声明。
如果大模型实在是笨的学不会传入正确数量的参数,那就只能将我们的工具函数修改为兼容动态参数的形式了。
代理执行器 executor 是 Agent 的运行时,可以理解为 AI Agent 的大脑,他协调各个组件并指导操作。这实际上是调用代理,执行它选择的操作,将操作输出传递回代理,然后重复。
class AgentExecutor {
agent: LLMSingleActionAgent
tools: StructuredTool[] = []
maxIterations: number = 15
constructor(agent: LLMSingleActionAgent) {
this.agent = agent
}
addTool(tools: StructuredTool | StructuredTool[]) {
const _tools = Array.isArray(tools) ? tools : [tools]
this.tools.push(..._tools)
}
}
最关键的就是 executor 的执行循环了,executor 会始终进行如下事件循环直到 目标被解决了 或者 思考迭代次数超过了最大次数:
async call(input: promptInputs): Promise<AgentFinish> {
const toolsByName = Object.fromEntries(
this.tools.map(t => [t.name, t]),
)
const steps: AgentStep[] = []
let iterations = 0
while (this.shouldContinue(iterations)) {
const output = await this.agent.plan(steps, input)
console.log(iterations, output)
// Check if the agent has finished
if ('returnValues' in output)
return output
const actions = Array.isArray(output)
? output as AgentAction[]
: [output as AgentAction]
const newSteps = await Promise.all(
actions.map(async (action) => {
const tool = toolsByName[action.tool]
if (!tool)
throw new Error(`${action.tool} is not a valid tool, try another one.`)
const observation = await tool.call(action.toolInput)
return { action, observation: observation ?? '' }
}),
)
steps.push(...newSteps)
iterations++
}
return {
returnValues: { output: 'Agent stopped due to max iterations.' },
log: '',
}
}
这里我们为大模型提供了加减乘除四个工具(按理来说这四个工具不需要实际的函数大模型也可以执行 hhhh,但本质上是不一样的)我们可以看到大模型最后成功迭代出了计算结果为 9336 元。
describe('agent', () => {
const llm = new AzureLLM({
apiKey: Config.apiKey,
model: Config.model,
})
const agent = new LLMSingleActionAgent({ llm })
agent.setPrompt(REACT_PROMPT)
agent.addStop(agent.observationPrefix)
agent.addTool([new AdditionTool(), new SubtractionTool(), new DivisionTool(), new MultiplicationTool()])
const executor = new AgentExecutor(agent)
executor.addTool([new AdditionTool(), new SubtractionTool(), new DivisionTool(), new MultiplicationTool()])
it('test', async () => {
const res = await executor.call({ input: '一种减速机的价格是 750 元,一家企业需要购买 12 台。每台减速机运行一小时的电费是 0.5 元,企业每天运行这些减速机 8 小时。请计算企业购买及一周运行这些减速机的总花费。' })
expect(res).toMatchInlineSnapshot(`
{
"log": "Final Answer: The total cost of purchasing and operating the gearboxes for a week is 9336 yuan.",
"returnValues": {
"output": "The total cost of purchasing and operating the gearboxes for a week is 9336 yuan.",
},
}
`)
}, { timeout: 50000 })
})
通过以上步骤,我们构建了一个基础的 ReAct Agent 框架。在实际应用中,可以根据具体需求扩展记忆模块、优化工具调用逻辑以及增强错误处理能力。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online