今年 3 月份,知名人工智能科学家吴恩达(Andrew Ng)在社交平台 X 上发帖提到,“我认为 AI 代理工作流程将在今年推动 AI 的大规模进步——甚至可能比下一代基础模型还要多......GPT-3.5(零样本)的正确率为 48.1%,GPT-4(零样本)为 67.0%,而在智能体循环中,GPT-3.5 的正确率高达 95.1%”。此贴发出之后,引发了业界广泛关注。有人表示,这代表着 AI 发展中的范式转变。
本文首先对智能体(AI Agent)的概念做一个简单介绍,然后详细拆解一个仅用 100 行代码构建的极简智能体应用。
1 什么是智能体?
智能体(AI Agent)是一种超越简单文本生成的人工智能系统,它使用大语言模型(LLM)作为其核心计算引擎,使其能够进行对话、推理、执行任务,展现一定程度的自主性。
在智能体架构中,核心功能可以归纳为三个步骤的循环:感知-决策-行动。智能体首先通过感知机制收集环境信息,然后基于该信息和预设目标,通过决策机制制定行动计划,最终通过动作执行机制实施这些计划。
图:智能体架构示意
2 示例:Mini AI Agent
了解了智能体的概念,接下来我们一起一步步拆解一个仅用 100 行代码构建的最小智能体应用,耗时约 1 个小时。
2.1 效果演示
先来看一下效果演示,
图:Mini AI Agent应用演示
看似平淡无奇的两次问答,实际上已经体现了智能体的核心循环:感知-决策-行动。
感知:接收问题
决策:理解问题,确定目标,然后通过推理决定使用何种工具(即制定计划)
行动:使用工具获取信息,然后生成答案
2.2 环境准备
申请账号:百度千帆
打开模型服务-应用接入页面,创建应用,记下 API Key 和 Secret Key 备用
打开模型服务-在线服务页面,找到 ERNIE-3.5-8K(支持函数功能),开通付费(不用担心,非常便宜,100 次调用才 2 毛钱)
PS: 本文只是以百度千帆为例,大家可以根据自身经验,替换成其他任何支持函数功能的大模型,比如智谱清言、Azure、OpenAI等。
搭建本地开发环境:Jupyter Notebook
访问 Anaconda 官网,下载安装包并安装 Anaconda
命令行运行
conda install jupyter notebook
,安装 Jupyter Notebook打开 GitHub 示例项目 emac/langchain-samples,git clone 到本地
命令行打开 langchain-samples 目录,运行
jupyter notebook
,打开 Jupyter Notebook
2.3 程序拆解
准备好环境之后,就可以进入程序员最喜欢的实操环节了!
安装依赖
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="cmd" cid="n647" mdtype="fences" style="box-sizing: border-box; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: pre; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none; position: relative !important; background-position: inherit; background-repeat: inherit;">!pip install langchain langchain-community langchain-core gradio</pre>
依赖说明:
初始化大模型
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="python" cid="n656" mdtype="fences" style="box-sizing: border-box; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: pre; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none; position: relative !important; background-position: inherit; background-repeat: inherit;">from langchain_community.llms import QianfanLLMEndpoint
import os
print("# 初始化千帆")
os.environ["QIANFAN_AK"]='千帆应用的 API Key'
os.environ["QIANFAN_SK"]='千帆应用的 Secret Key'
llm = QianfanLLMEndpoint(streaming=True,
model="ERNIE-3.5-8K",
temperature=0.1)
response = llm.invoke("上海春天一般哪个月开始?")
print(response)</pre>
执行之前先替换之前记录的千帆应用的 API Key 和 Secret Key。
程序解读:
初始化环境变量,创建一个千帆 LLM 实例
发起(人生)第一次大模型 API 调用,如果不成功则返回检查环境和依赖
定义函数
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="python" cid="n667" mdtype="fences" style="box-sizing: border-box; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: pre; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none; position: relative !important; background-position: inherit; background-repeat: inherit;">from langchain import PromptTemplate, LLMChain
from langchain.chains import LLMRequestsChain
from langchain_core.tools import tool
from langchain_core.utils.function_calling import convert_to_openai_tool
print("# 定义函数")
@tool
def search_ip(question:str, ip:str) -> str:
"""
首先获取输入的IP地址的位置信息,然后回答输入的问题
@param question: 问题
@param ip: IP地址
"""
prompt_template = """以下是IP地址'{ip}'的位置信息:
{requests_result} <<<
根据以上位置信息,回答以下这个问题:
{question} <<<"""
prompt = PromptTemplate(
input_variables=["question", "ip", "requests_result"],
template=prompt_template
)
chain = LLMRequestsChain(llm_chain = LLMChain(llm=llm, prompt=prompt))
inputs = {
"question": question,
"ip": ip,
"url": "https://api.songzixian.com/api/ip?dataSource=generic_ip&ip=" + ip
}
return chain.invoke(inputs)
@tool
def search_phone(question:str, phone:str) -> str:
"""
首先获取输入的手机号码的归属地信息,然后回答输入的问题
@param question: 问题
@param phone: 手机号码
"""
prompt_template = """以下是手机号码'{phone}'的归属地信息:
{requests_result} <<<
根据以上归属地信息,回答以下这个问题:
{question} <<<"""
prompt = PromptTemplate(
input_variables=["question", "phone", "requests_result"],
template=prompt_template
)
chain = LLMRequestsChain(llm_chain = LLMChain(llm=llm, prompt=prompt))
inputs = {
"question": question,
"phone": phone,
"url": "https://api.songzixian.com/api/phone-location?dataSource=phone_number_location&phoneNumber=" + phone
}
return chain.invoke(inputs)
functions=[convert_to_openai_tool(search_ip)['function'],convert_to_openai_tool(search_phone)['function']]
print(functions)</pre>
程序解读:
定义
search_ip
和search_phone
两个工具函数,背后连接一个免费的第三方 API 接口平台,用来获取指定 IP 的位置信息和指定手机号的归属地信息,然后再结合原始问题,通过大模型生成最终回答。tool/ convert_to_openai_tool: 用于生成函数定义的注解和工具方法
PromptTemplate: Prompt 模板,支持变量
LLMRequestsChain: 一个基于 URL 请求的大模型链,先调用 URL 获取数据,然后将数据传给一个已绑定 Prompt 的大模型链获取答案
绑定函数
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="python" cid="n680" mdtype="fences" style="box-sizing: border-box; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: pre; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none; position: relative !important; background-position: inherit; background-repeat: inherit;">from langchain.schema import HumanMessage
from langchain_community.chat_models import QianfanChatEndpoint
import json
此处不能设定streaming=True,否则无法激活函数回调
chat = QianfanChatEndpoint(model="ERNIE-3.5-8K",
temperature=0.1)
def get_response(message):
print("# 绑定函数")
result = chat.invoke([HumanMessage(content=message)],
functions=functions)
print(result)
function_call_info = result.additional_kwargs.get("function_call", None)
print(function_call_info)
if not function_call_info:
print("# 直接返回")
return result.content
print("# 调用函数")
function_name = function_call_info["name"]
function_args = json.loads(function_call_info["arguments"])
function_result = eval(function_name)(function_args)
print(function_result)
return function_result["output"]</pre>
程序解读:
-
创建一个千帆 Chat 实例,绑定之前定义的两个函数,传入用户输入的问题(对应智能体的感受环节),然后发起调用,根据调用结果执行不同任务(对应智能体的决策环节):
如果调用结果没有提示函数调用(
result.additional_kwargs.get("function_call", None)
),则直接返回结果否则,根据大模型提示的函数名和请求参数,通过反射调用相应的函数(对应智能体的执行环节),然后返回结果
构建应用
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="Python" cid="n692" mdtype="fences" style="box-sizing: border-box; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: pre; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none; position: relative !important; background-position: inherit; background-repeat: inherit;">import gradio as gr
def submit(message, chat_history):
bot_message = get_response(message)
保存历史对话记录,用于显示
chat_history.append((message, bot_message))
return "", chat_history
print("# 创建交互")
with gr.Blocks() as demo:
chatbot = gr.Chatbot(height=240) # 对话框
msg = gr.Textbox(label="Prompt") # 输入框
submitBtn = gr.Button("Submit") # 提交按钮
clearBtn = gr.ClearButton([msg, chatbot]) # 清除按钮
提交
msg.submit(submit, inputs=[msg, chatbot], outputs=[msg, chatbot])
submitBtn.click(submit, inputs=[msg, chatbot], outputs=[msg, chatbot])
gr.close_all()
demo.launch()</pre>
程序解读:
创建智能体应用,构建一个用户和智能体的对话框,将用户输入的消息传给后台创建的大模型,实时获取响应
此处可以看到,借助 Gradio 框架,短短几行代码,就可以构建出一个简洁的对话框应用,非常 Nice!
至此,一个极简的智能体应用就构建成功了,前后仅用 100 行代码。完整代码参见 GitHub。
彩蛋:智能体如何思考?
看完演示,拆解完程序,你可能对智能体里的“智能“两字的理解还是有点模模糊糊。其实要真正理解这一点,光看程序还不够,得看下大模型的响应结果(见下)。注意其中 response_metadata
有一个很有意思的 thoughts
字段,其代表了大模型的思考过程,是不是和人类非常类似?
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="json" cid="n705" mdtype="fences" style="box-sizing: border-box; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: pre; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none; position: relative !important; background-position: inherit; background-repeat: inherit;"># 问题:120.230.93.202属于哪个城市?
大模型响应结果
additional_kwargs={'finish_reason': 'function_call', 'request_id': 'as-fcd0dz2mmg', 'object': 'chat.completion', 'search_info': [], 'function_call': {'name': 'search_ip', 'arguments': '{"question":"120.230.93.202属于哪个城市?","ip":"120.230.93.202"}'}}
response_metadata={'token_usage': {'prompt_tokens': 194, 'completion_tokens': 63, 'total_tokens': 257}, 'model_name': 'ERNIE-3.5-8K', 'finish_reason': 'function_call', 'id': 'as-fcd0dz2mmg', 'object': 'chat.completion', 'created': 1714900176, 'result': '', 'is_truncated': False, 'need_clear_history': False, 'function_call': {'name': 'search_ip', 'thoughts': '用户想要知道一个特定IP地址的地理位置信息,我需要使用search_ip工具来获取这个信息。', 'arguments': '{"question":"120.230.93.202属于哪个城市?","ip":"120.230.93.202"}'}, 'usage': {'prompt_tokens': 194, 'completion_tokens': 63, 'total_tokens': 257}} id='run-0706ab6f-a21f-4352-bbb1-1f1a0f13c426-0'</pre>
3 小结
本文基于百度千帆大模型,使用 Langchain 和 Gradio 框架,用短短 100 行代码就构建出一个极简的智能体应用。该应用能够根据用户问题,选择不同的工具获取信息,并生成最终回答,体现了智能体最核心的三步循环:感知-决策-行动。基于这个演示应用,相信聪明的你可以构建出更复杂、更智能的智能体应用,欢迎留言交流。