问你一个问题,你能清晰的说明什么是web应用吗?大多数程序员估计会轻蔑一笑,不就web吗,谁不知道。但如果我问你什么是LLM应用,你可能会像是coze?好像不太对,是cursor?manus?豆包?好像都不太对。没错,现在市面上对LLM应用基本上是没有一个明确的定义的,今天我们就来尝试性的探讨一下,到底什么是LLM应用。
什么是LLM应用
提到一个web应用,最简单的结构是,前端页面+服务端+数据库,这里不提什么分布式微服务啥的,因为那不是重点,那LLM应用是不是应该也有一些核心的组成部分?没错,下面我们就来看一下LLM应用的关键构成
LLM应用的关键构成要素
模型
首先是LLM,LLM应用嘛,没有LLM哪行,在传统开发场景中,我们想收集用户数据,最起码要提供一个表单,但如果用户给我们的是一段没有固定格式的话,该怎么办?这就是LLM要解决的问题。
模型的作用
LLM提供的是智力基础,解决的是人或者自然语言等等更加不确定性的问题,依靠的是模型的理解能力,判断能力,语言组织能力等等能力。
落地场景
大家在市面上常见的ChatBot就是典型的应用场景,你提个问题,它会自己去理解分析并且给出回复
但是,LLM存在一个致命的问题,就是大家常说的模型幻觉,这个幻觉和我们人的幻觉不一样,人的幻觉是感官出问题,LLM出幻觉是因为LLM在训练完成后参数就被冻结了,如果说提出的问题使用了训练时没有给到的语料,它就会按照概率给出一个最可能的回答,也就我们常说的瞎编。
既然模型的参数是冻结的,新的知识它不知道,那我们给它外挂个知识库比就好了,没错,这就是LLM应用的第二个核心部分——知识
数据
数据主要是用来提供模型需要的知识,模型在训练的时候一般使用的都是公开的语料或者现在已经有的语料,如果是未来的知识,比如明天的新闻,或者某些私有的数据,比如某企业的用户清单,它肯定是不知道的,所以我们主要需要补充的就是两方面的数据:
- 时效性信息
- 非公开信息
知识的作用
其实比较简单,一个是为了消除幻觉,另一个补足参数冻结的短板。
落地场景
有了补充的知识,再加上模型的上下文学习能力,现在我们就可以实现智能客服,或者某个领域专用对的ChatBot了,但是这样还是有问题,模型还是只能拿来对话,充其量就是个聊天机器人。所以我们就还需要的三部分
输出控制
通俗点说就是行为控制,怎么理解,前面两点模型+知识,我们可以认为模型是一个聪明的大脑,现在我们要给它装上"手和脚"。
输出控制的作用
使模型不再局限于对话场景,这里依靠的是模型的行动规划,任务分发,工具调用(前面文章提到的FunctionCall)等等能力,最终的目的是要让模型对的智能+数据,形成真正的生产力,所谓的LLM应用,核心工作其实是在这一块
落地场景
典型的落地场景比如个人助理,短视频流水线等等
结合上面的三点,我们在你以为的智能体真的是智能体吗中提到的智能体其实是LLM应用的一种形式。可以用来处理不确定性更强的问题。
有人可能要说了,你文章中的智能体不智能啊,规划,行动,反思,步骤都是你指定了的,不是智能体自己规划出来的。
没错其实我们在你以为的智能体真的是智能体吗这篇文章中的实践,更多的应用的是工作流的思想,但是工作流不等于不智能。下面我们来看一下为什么。
工作流
单次请求的局限性
首先为什么会有工作流,这是因为单次的模型请求存在很大的局限性,比如说:
- 上下文窗口长度限制,输出长度的限制,为了解决这个问题,早期LangChain基于Map-Reduce思想对长文本做过处理
- 如果使用思维链的话(尤其是自然语言的思维链),会输出思考过程,有的场景用户不希望看到,而且比较耗token
- 有的时候,下一步任务的信息是依赖上一步执行的,但是单次请求不一定能获取到
因为有以上的局限性,所以就有了工作流这个东西
工作流的优势
工作流一般是基于SOP的,所以工作流的第一个优势就是,可以将任务拆分成不同的节点,单次的模型请求也只是其中的一个节点,而且有了一堆节点以后,我们可以随意的在任何一个节点前后穿插代码,调整节点顺序,传递数据等等 基于以上优势,工作流也有一些构成要素
工作流的要素
基本要素
- 要有工作节点
- 节点和节点要能够相互连接,不管是顺讯连接还是条件连接
- 可以传递数据,不管是节点和节点之间,还是整个工作流的全局数据流转
进阶要求
现在有了工作节点,节点之间有链接,能够互相传数据,这就够了吗,显然不是,在实际开发工作中,流程要比这复杂的多,比如有循环,有并行,有路由分发等等,所以工作流也有一些进阶要求
- 能够成环
- 成环就标志着能够进行任务重试,能够根据需要多次执行任务,但是要注意需要给退出条件,不然会出现死循环
- 能够按条件分发
- 在传统开发中有一个非常常见的场景,就是我们在一个服务中处理完数据,需要根据某些条件分发给不同的处理流程,LLM下的工作流也一样,需要能够根据推理结果交给下游不同的工作流或应用去执行。
- 能够并行执行并且在某个节点等待
- 实际场景中我们经常需要并行执行一些任务,比如去三个不同的服务中获取数据,然后整合再执行后续工作。
- 能够对一个任务清单拆分并回收结果
- 也就是可以将一个list任务清单中的任务逐项处理,或者将每一项单独处理,最后合并结果
- 可以进行复杂通讯
- 传统开发工作中我们经常会用全局变量控制一些状态,对于工作流也是类似
- 并发安全问题,在复杂工作流中,经常会出现多个节点需要修改同一个全局数据,怎么保证节点B能正确的获取到节点A生成的数据也非常重要
误区:要智能就不要工作流
有的人可能会说,说了半天不还是得人为规划吗?这也不智能啊,没错,工作流确实有它的适用场景,比如SOP非常明确的场景,但是点一杯咖啡有SOP,那如果我抽象成只要点咖啡都能用的SOP呢?
有人又要说了,那不还是SOP,我不管,我就要LLM应用自己想怎么干!!!
没问题,这个也有人搞过,叫自规划
自规划
首先我们来看一个自规划的祖师爷级别的应用
AutoGPT
AutoGpt工作过程
下面我们来从Prompt角度来拆解一下AutoGpt是如何工作的
关键Prompt解读
这是一个早期版本的prompts生成逻辑AutoGPT v0.3.0版本(2023.5)
基本限制
CONSTRAINTS:
1. ~4000 word limit for short term memory. Your short term memory is short, so immediately save important information to files.
2. If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember.
3. No user assistance
4. Exclusively use the commands listed in double quotes e.g. "command name”
- 提示记忆机制:4000字上下文窗口很短,需要将重要信息保存到文件,需要时尝试回忆
- 提示自动性:不能由用户辅助完成任务
- 提示工具使用:可以通过命令,使用环境提供的工具
在23年的时候,大模型的上下文窗口还很小,所以使用了提示词来保证memory
可用工具说明
COMMANDS:
1. Google Search: "google", args: "input": "<search>"
5. Browse Website: "browse_website", args: "url": "<url>", "question": "<what_you_want_to_find_on_website>"
6. Start GPT Agent: "start_agent", args: "name": "<name>", "task": "<short_task_desc>", "prompt": "<prompt>"
7. Message GPT Agent: "message_agent", args: "key": "<key>", "message": "<message>"
8. List GPT Agents: "list_agents", args: ""
9. Delete GPT Agent: "delete_agent", args: "key": "<key>"
10. Write to file: "write_to_file", args: "file": "<file>", "text": "<text>"
11. Read file: "read_file", args: "file": "<file>"
12. Append to file: "append_to_file", args: "file": "<file>", "text": "<text>"
13. Delete file: "delete_file", args: "file": "<file>"
14. Search Files: "search_files", args: "directory": "<directory>"
15. Evaluate Code: "evaluate_code", args: "code": "<full_code_string>"
16. Get Improved Code: "improve_code", args: "suggestions": "<list_of_suggestions>", "code": "<full_code_string>"
17. Write Tests: "write_tests", args: "code": "<full_code_string>", "focus": "<list_of_focus_areas>"
18. Execute Python File: "execute_python_file", args: "file": "<file>"
19. Task Complete (Shutdown): "task_complete", args: "reason": "<reason>"
20. Generate Image: "generate_image", args: "prompt": "<prompt>"
21. Do Nothing: "do_nothing", args: “"
- 工具表达结构:
工具名: "指令名", args: "参数名": "<参数定义>"
看着眼熟不?到现在为止,很多框架的工具描述其实都有受AutoGPT的影响
可用资源提示
RESOURCES:
1. Internet access for searches and information gathering.
2. Long Term memory management.
3. GPT-3.5 powered Agents for delegation of simple tasks.
4. File output.
- 对资源使用方式的提示(提示工具可以如何使用)
- 可以搜索访问互联网信息
- 可以使用长期记忆管理机制
- 可以使用GPT-3.5驱动的Agents作为辅助处理更简单的问题(当时的中控是GPT-4)
- 可以输出文件
表现评估提示
PERFORMANCE EVALUATION:
1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities.
2. Constructively self-criticize your big-picture behavior constantly.
3. Reflect on past decisions and strategies to refine your approach.
4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.
- 为反思和下一步规划提供指导
- 复盘过去的行动判断是否已经是最优表现
- 提醒自我反思行为决策来优化表现
- 提醒成本控制(省token很重要!!!)
输出要求
You should only respond in JSON format as described below
RESPONSE FORMAT:
{
"thoughts":
{
"text": "thought",
"reasoning": "reasoning",
"plan": "- short bulleted\n- list that conveys\n- long-term plan",
"criticism": "constructive self-criticism",
"speak": "thoughts summary to say to user"
},
"command": {
"name": "command name",
"args":{
"arg name": "value"
}
}
}
Ensure the response can be parsed by Python json.loads
- 给出最终输出格式要求(工程化的基础) 这是第一次在大模型的输出中,出现以json为格式的结构化数据,写过代码的都知道结构化数据很重要,这项操作影响了后续所有的主流模型,也是从这儿开始,所有的主流模型开始添加json结构化输出能力
AutoGPT的出现的意义不只是上面的提示词,虽然因为23三年大模型的能力还没有现在强,外部工具也没有这么丰富,经常出现死循环的情况,但是它还让人们看到,原来模型还能这么用,原来模型不只是陪聊。
代码环节
好了,哔哔这么半天,该上点代码了,面我们来实现一个查询助手的自规划应用,下面是我们要完成的任务
- 解析用户的想查询的信息
- 通过搜索知道资料位置
- 浏览获取资料
- 输出结果
环境准备
pip install requests
pip install beautifulsoup4
pip install dotenv
pip install nest_asyncio
定义需要的工具
import os
import json
import requests
import re
from bs4 import BeautifulSoup
# 搜索工具
def search(keywords:list):
payload = json.dumps({
"q": ' '.join(keywords),
})
headers = {
# 访问serper.dev注册账号获取API Key
'X-API-KEY': os.environ["SERP_API_KEY"],
'Content-Type': 'application/json'
}
response = requests.request("POST", "https://google.serper.dev/search", headers=headers, data=payload)
return response.text
# 浏览工具
def browse(url: str):
content = ""
try:
request_options = {
"headers": { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" }
}
page = requests.get(
url,
**request_options
)
soup = BeautifulSoup(page.content, "html.parser")
# find text in p, list, pre (github code), td
chunks = soup.find_all(["h1", "h2", "h3", "h4", "h5", "p", "pre", "td"])
for chunk in chunks:
if chunk.name.startswith("h"):
content += "#" * int(chunk.name[-1]) + " " + chunk.get_text() + "\n"
else:
text = chunk.get_text()
if text and text != "":
content += text + "\n"
# find text in div that class=content
divs = soup.find("div", class_="content")
if divs:
chunks_with_text = divs.find_all(text=True)
for chunk in chunks_with_text:
if isinstance(chunk, str) and chunk.strip():
content += chunk.strip() + "\n"
content = re.sub(r"\n+", "\n", content)
return content
except Exception as e:
return f"Can not browse '{ url }'.\tError: { str(e) }"
# 工具说明信息
tools_info = [
{
"name": "search",
"desc": "使用网络搜索工具,搜索{keywords}指定关键词相关信息",
"kwargs": {
"keywords": [("str", "一个关键词")],
},
},
{
"name": "browse",
"desc": "使用浏览工具,浏览{url}指定的页面内容",
"kwargs": {
"url": ("str", "可访问的URL地址")
},
},
]
# 存放真正的工具函数
tools_mapping = {
"search": search,
"browse": browse,
}
定义自规划工作流
工作流设计
- 存储用户输入信息,用于后续反思是否完成任务
- 制定下一步任务
- 用户输入(来源于用户,外部提供)
- 提供可用工具清单(来源于开发者,外部提供)
- 已执行的步骤(行动记忆,来自于任务执行过程持续积累,工具和模型配合提供)
- 条件分支 2. 用工具--返回计划制定部分判断是否需要继续调用工具 3. 不需要--跳出循环进入下一步
- 给出回复
# 处理notebook环境asyncio冲突问题
import nest_asyncio
nest_asyncio.apply()
import asyncio
import Agently
auto_workflow = Agently.Workflow()
@auto_workflow.chunk()
def save_user_input(inputs, storage):
storage.set("user_input", inputs["default"])
return
@auto_workflow.chunk()
def make_next_plan(inputs, storage):
# 这个操作是为了后续将工作流附着到agent对象准备的
# 如果不需要附着,可以像上节课代码一样,直接创建一个新的agent实例
agent = storage.get("$agent")
user_input = storage.get("user_input") # 用户输入(目标)
tools_info = storage.get("tools_info", []) # 获取可用工具清单
done_plans = storage.get("done_plans", []) # 获取行动记忆
result = (
agent
.input(user_input)
.info({
"可用工具清单": tools_info,
"已经做过": done_plans,
})
.instruct([
"根据{input}的用户意图,{已经做过}提供的行动记录以及{可用工具清单}提供的工具,制定解决问题的下一步计划",
"如果{已经做过}提供的行动记录中,某项行动反复出现错误,可将下一步计划定为'输出结果',回复内容为对错误的说明",
])
.output({
"next_step_thinking": ("str", ),
"next_step_action": {
"type": ("'工具使用' | '输出结果'", "MUST IN values provided."),
"reply": ("str", "if {next_step_action.type} == '输出结果',输出你的最终回复结果,else输出''"),
"tool_using": (
{
"tool_name": ("str from {可用工具清单.name}", "必须使用{可用工具清单}提供的工具"),
"purpose": ("str", "描述使用工具希望解决的问题"),
"kwargs": ("dict,根据{可用工具清单.kwargs}要求给出所需参数"),
},
"if {next_step_action.type} == '工具使用',给出你的工具使用计划说明,else输出null",
),
}
})
.start()
)
if storage.get("print_process"):
print("💡 我觉得下一步应该:", result["next_step_thinking"])
return result["next_step_action"]
@auto_workflow.chunk()
async def use_tool(inputs, storage):
tool_using_info = inputs["default"]["tool_using"]
tools_mapping = storage.get("tools_mapping")
tool_func = tools_mapping[tool_using_info["tool_name"]]
# 输出工具使用思路
if storage.get("print_process"):
print("[🪛 我觉得需要使用工具]:")
print("🤔 我想要解决的问题是:", tool_using_info["purpose"])
print("🤔 我想要使用的工具是:", tool_using_info["tool_name"])
# 使用工具(同异步函数兼容)
if asyncio.iscoroutine(tool_func):
tool_result = await tool_func(**tool_using_info["kwargs"])
else:
tool_result = tool_func(**tool_using_info["kwargs"])
# 输出工具使用结果
if storage.get("print_process"):
print("🎉 我得到的结果是:", tool_result[:100], "...")
# 将工具使用记录加入行动记忆
done_plans = storage.get("done_plans", [])
done_plans.append({
"purpose": tool_using_info["purpose"],
"tool_name": tool_using_info["tool_name"],
"result": tool_result,
})
storage.set("done_plans", done_plans)
return
@auto_workflow.chunk()
def reply(inputs, storage):
if storage.get("print_process"):
print("[💬 我觉得可以回复了]:")
print("✅ 我得到的最终结果是:", inputs["default"]["reply"])
return {
"reply": inputs["default"]["reply"],
"process_results": storage.get("done_plans"),
}
(
auto_workflow
.connect_to("save_user_input")
.connect_to("make_next_plan")
.if_condition(lambda return_value, storage: return_value["type"] == "输出结果")
.connect_to("reply")
.connect_to("end")
.else_condition()
.connect_to("use_tool")
.connect_to("make_next_plan")
)
auto_workflow.draw(type="img")
附着工作流给Agent
# 创建一个新的Agent实例,将工作流附着给它
auto_agent = (
Agently.create_agent()
.set_settings("current_model", "OAIClient")
.set_settings("model.OAIClient.url", "https://api.deepseek.com/")
.set_settings("model.OAIClient.auth", { "api_key": os.environ["DEEPSEEK_API_KEY"] })
.set_settings("model.OAIClient.options", { "model": "deepseek-chat" })
)
auto_agent.attach_workflow("auto", auto_workflow)
RUN
result = auto_agent.auto(
"今天广州哪里适合约会",
tools_info=tools_info,
tools_mapping=tools_mapping,
print_process=True
)
print("==========")
print(result["default"]["reply"])
运行结果
💡 我觉得下一步应该: 用户询问今天广州哪里适合约会,需要查找相关信息。由于尚未进行任何行动,下一步计划是使用搜索工具查找相关信息。
[🪛 我觉得需要使用工具]:
🤔 我想要解决的问题是: 查找今天广州适合约会的地点
🤔 我想要使用的工具是: search
🎉 我得到的结果是: {"searchParameters":{"q":"广 州 今 天 适 合 约 会 的 地 方","type":"search","engine":"google"},"organic":[{ ...
💡 我觉得下一步应该: 根据用户输入和已经做过的搜索记录,发现搜索结果中已经包含了一些适合约会的地点信息,如广州塔的摩天轮、沙面大街、上下九等。因此,下一步可以浏览这些具体地点的详细信息,以提供更具体的约会建议。
[🪛 我觉得需要使用工具]:
🤔 我想要解决的问题是: 浏览广州塔的摩天轮、沙面大街、上下九等具体地点的详细信息,以提供更具体的约会建议。
🤔 我想要使用的工具是: browse
🎉 我得到的结果是: ####
探索小当家
#
广州情侣必去的20个约会圣地!带对象打卡完,就结婚吧!
浪漫的520快到了
范爷整理了20个一定要 ...
💡 我觉得下一步应该: 根据用户输入和已经做过的行动记录,已经搜索到了广州适合约会的地点,并浏览了部分具体地点的详细信息。下一步可以整理这些信息,提供一个综合的约会地点推荐列表。
[💬 我觉得可以回复了]:
✅ 我得到的最终结果是: 根据搜索结果,以下是广州今天适合约会的地点推荐:
1. **广州塔摩天轮**:在浪漫的球舱内俯瞰城市美景,适合情侣约会。
- 地址:广州市海珠区阅江西路222号
- 营业时间:09:30-22:30
2. **太古仓码头**:江边音乐餐吧,适合散步、看日落、听音乐。
- 地址:广州市海珠区革新路1324号
- 营业时间:全天开放
3. **沙面大街**:欧陆风情建筑,适合拍照和散步。
- 地址:广州市荔湾区沙面
- 营业时间:全天开放
4. **永庆坊**:老西关文化街区,有手工集市、园林和网红打卡点。
- 地址:广州市荔湾区恩宁路与永庆大街交汇处
- 营业时间:全天开放
5. **白云山**:可以爬山或玩刺激的飞索项目。
- 地址:广州市白云区大金钟路与白云大道南交汇处东北侧
- 营业时间:全天开放
6. **长隆水上乐园**:夏日玩水的好去处,适合喜欢刺激的情侣。
- 地址:广州市番禺区长隆大道199号
- 营业时间:10:00-19:00
7. **艺术博物馆**:小众有格调的约会地点,适合喜欢安静的情侣。
- 地址:广州市越秀区麓湖路13号
- 营业时间:09:00-17:00
8. **超级文和友**:现代与怀旧融合的美食打卡地。
- 地址:广州市天河区天河东路75号
- 营业时间:11:00-03:00
可以根据你们的兴趣选择适合的地点,祝你们约会愉快!
==========
根据搜索结果,以下是广州今天适合约会的地点推荐:
1. **广州塔摩天轮**:在浪漫的球舱内俯瞰城市美景,适合情侣约会。
- 地址:广州市海珠区阅江西路222号
- 营业时间:09:30-22:30
2. **太古仓码头**:江边音乐餐吧,适合散步、看日落、听音乐。
- 地址:广州市海珠区革新路1324号
- 营业时间:全天开放
3. **沙面大街**:欧陆风情建筑,适合拍照和散步。
- 地址:广州市荔湾区沙面
- 营业时间:全天开放
4. **永庆坊**:老西关文化街区,有手工集市、园林和网红打卡点。
- 地址:广州市荔湾区恩宁路与永庆大街交汇处
- 营业时间:全天开放
5. **白云山**:可以爬山或玩刺激的飞索项目。
- 地址:广州市白云区大金钟路与白云大道南交汇处东北侧
- 营业时间:全天开放
6. **长隆水上乐园**:夏日玩水的好去处,适合喜欢刺激的情侣。
- 地址:广州市番禺区长隆大道199号
- 营业时间:10:00-19:00
7. **艺术博物馆**:小众有格调的约会地点,适合喜欢安静的情侣。
- 地址:广州市越秀区麓湖路13号
- 营业时间:09:00-17:00
8. **超级文和友**:现代与怀旧融合的美食打卡地。
- 地址:广州市天河区天河东路75号
- 营业时间:11:00-03:00
可以根据你们的兴趣选择适合的地点,祝你们约会愉快!
总结
这篇文章我们讨论了什么是LLM应用,进一步探讨了适合确定性问题的工作流和适合开放问题的自规划Agent,并且实现了一个自规划的信息查询助手。
总之不管是工作流还是自规划,也不论是单智能体还是多智能体,都只是LLM应用解决方案中的一个细分思路,更重要的是,如何更好的掌握模型能力,将模型能力应用于具体的业务场景产生实际的业务价值。