参考文章:
datawhalechina/hello-agents: 📚 《从零开始构建智能体》------从零开始的智能体原理与实践教程
其实这个教程已经讲述的非常详细了,这了这是添加一些自己的理解。
# 制作工具
这里制作了两个mcp工具,get_weather和get_attraction,功能分别是查询天气和根据天气获得景区的推荐。
python
import requests
import pprint
# 定义第一个工具:查询天气工具
def get_weather(city:str)-> str:
# 获取查询查询的链接:https://wttr.in/
url = f"https://wttr.in/{city}?format=j1"
try:
response = requests.get(url)
response.raise_for_status()
# 解析数据
data = response.json()
current_condition = data['current_condition'][0] # 返回字典类型,根据"键"获得需要的数据
weather = current_condition["weatherDesc"][0]["value"] # 获得天气
temp_c = current_condition["temp_C"]
#pprint.pprint(current_condition)
return f"当前{city}地区的天气是:{weather},气温是:{temp_c}摄氏度!"
except requests.exceptions.RequestException as e:
return f"错误:查询天气是遇到网络问题-{e}"
except (KeyError,IndexError) as e:
return f"错误:解析天气数据失败!-{e}"
# city = "桂林"
# info = get_weather(city)
# print(info)
python
# 定义第二个工具:搜索推荐旅游景点工具
# 这是一个基于大模型的工具,和后面负责调度,自行规划判断的主大模型要区分开来
import os
from openai import OpenAI
def get_attraction(city:str,weather:str)->str:
api_key = os.getenv("ARK_API_KEY")
client = OpenAI(
base_url="https://ark.cn-beijing.volces.com/api/v3",
api_key=api_key,
)
problem = f"你是一个旅游助手,请你为我推荐:{city}在{weather}天气下最值得去的至多3个旅游景点推荐及理由,回答要精简,结构化输出!"
# 纯文本对话请求(最简版本)
try:
response = client.responses.create(
model="doubao-seed-2-0-pro-260215",
# 直接传入纯文本字符串即可
input= problem
)
# print(type(response))
# print(response)
return response.output_text
except Exception as e:
return f"出现错误-{e}!"
# city = "桂林"
# weather = get_weather(city)
# result = get_attraction(city,weather) # 获得大模型的返回结果
# print(result)
原本的教程中获得景区推荐的函数是基于tavily实现的,这个需要注册他们公司的账号,然后领取免费的额度,然后获得景区推荐。我这里是另外基于豆包大模型获得景区推荐的。本质上算是封装乘一个工具吧。
将制作好的函数工具封装成字典,后续通过字典进行函数映射,来调用对应的工具
python
# 将设计好的工具放入字典,方便后续大模型自主判断使用
available_tools = {"get_weather":get_weather,"get_attraction":get_attraction}
# 封装一个统一的大模型调用接口
python
# 定义一个通用的客户端类,避免每次调用大模型重复初始化工作
class OpenAIClient:
def __init__(self,model,api_key,base_url):
self.model = model
self.client = OpenAI(api_key=api_key,base_url=base_url)
def generate(self,prompt,system_prompt):
print("正在调用大语言模型...")
try:
messages = [
{"role":"system","content":system_prompt},
{"role":"user","content":prompt}
]
response = self.client.chat.completions.create(
model = self.model,
messages=messages,
stream = False
)
answer = response.choices[0].message.content
print("大语言模型相应成功!")
return answer
except Exception as e:
return f"调用大模型API时发生报错!-{e}"
# 通过提供三个信息实例化对象
'''
model_id
api_key
base_url
'''
后续通过实例化,来和大模型进行交互。
# 创建一个系统提示词
这个是关键,指定了大模型的输出规范,交代了大模型可以使用的工具函数。
python
AGENT_SYSTEM_PROMPT = """
你是一个智能旅行助手。你的任务是分析用户的请求,并使用可用工具一步步地解决问题。
# 可用工具:
- `get_weather(city)`: 查询指定城市的实时天气。
- `get_attraction(city,weather)`: 根据城市和天气搜索推荐的旅游景点。
# 输出格式要求:
你的每次回复必须严格遵循以下格式,包含一对Thought和Action:
Thought: [你的思考过程和下一步计划]
Action: [你要执行的具体行动]
Action的格式必须是以下之一:
1. 调用工具:function_name(arg_name="arg_value")
2. 结束任务:Finish[最终答案]
# 重要提示:
- 每次只输出一对Thought-Action
- Action必须在同一行,不要换行
- 当收集到足够信息可以回答用户问题时,必须使用 Action: Finish[最终答案] 格式结束
请开始吧!
"""
# 主函数
这里简单谈一下这一部分实现的思路,总体看下来还是比较妙的。他没有基于专业的如langchain这样的agent开发框架,而是从底层一步一步实现的。
**对话历史的管理:**每次对话的内容都会存储在一个列表中,这样大模型就能够知道自己当前工作的状态,分析到哪一步了。所以从最终输出结果来看,整个大模型的工作过程就是一步一步思考的。
**MCP工具的使用:**大模型不能够使用工具,但他知道应该使用哪一个工具。所以通过规范化输出,提取出使用的函数,具体函数的操作还是通过python后端调用函数实现的。此外,这里是通过正则表达式的方式,提取出关键的函数名称和形参的,通过这样的方式准确调用工具函数。
python
# 实例化对象,调用大模型
import re # 正则表达式
MODEL_ID = "doubao-seed-2-0-pro-260215"
API_KEY_MAIN = os.getenv("ARK_API_KEY")
BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
llm = OpenAIClient(
model = MODEL_ID,
api_key= API_KEY_MAIN,
base_url= BASE_URL
)
user_prompt = "我今天想去桂林玩,是否值得去,如果去的话可以去哪些地方玩?"
prompt_history = [f"用户请求:{user_prompt}"] # 存储用户的提问
print(f"用户输入:{user_prompt}\n"+"="*40)
# 运行主程序
for i in range(5):
print(f"-----循环{i+1}-----\n")
# print("*"*40)
# print("查看history_prompt中的情况:\n")
full_prompt = "\n".join(prompt_history) # 将历史内容拼接,因为目前没有记忆功能
# print(full_prompt)
# print("*"*40)
llm_output = llm.generate(full_prompt,system_prompt=AGENT_SYSTEM_PROMPT)
print("llm_output的情况是:之后match在这个基础上进行匹配",llm_output)
# 这里因为没有使用langchain这样的框架,所以其实只能够通过正则表达式的方式来调用mcp工具,并不能自动控制调用工具
match = re.search(r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)', llm_output, re.DOTALL)
# print("*"*40)
# print("match匹配的结果为:\n")
# print(match)
# print("*"*40)
# 有一点不太理解这里还有截断的必要吗 答:match匹配的结果不干净,这里相当与是进一步整理,使得输出结果更加结构化,规范
if match:
truncated = match.group(1).strip()
if truncated != llm_output.strip():
llm_output = truncated
print("已截断多余的 Thought-Action 对")
# print(f"模型输出:\n{llm_output}\n")
prompt_history.append(llm_output)
# print("经过处理之后match的情况!如上")
action_match = re.search(r"Action: (.*)", llm_output, re.DOTALL)
if not action_match:
observation = "错误: 未能解析到 Action 字段。请确保你的回复严格遵循 'Thought: ... Action: ...' 的格式。"
observation_str = f"Observation: {observation}"
print(f"{observation_str}\n" + "="*40)
prompt_history.append(observation_str)
continue
action_str = action_match.group(1).strip()
if action_str.startswith("Finish"):
# final_answer = re.match(r"Finish\[(.*)\]", action_str).group(1)
finish_match = re.match(r"Finish\[(.*?)\]", action_str, re.DOTALL)
if finish_match:
final_answer = finish_match.group(1)
else:
final_answer = action_str
print(f"任务完成,最终答案: {final_answer}")
break
tool_name = re.search(r"(\w+)\(", action_str).group(1)
args_str = re.search(r"\((.*)\)", action_str).group(1)
kwargs = dict(re.findall(r'(\w+)="([^"]*)"', args_str))
if tool_name in available_tools:
observation = available_tools[tool_name](**kwargs)
else:
observation = f"错误:未定义的工具 '{tool_name}'"
# 3.4. 记录观察结果
observation_str = f"Observation: {observation}"
print(f"{observation_str}\n" + "="*40)
prompt_history.append(observation_str)