1. 项目概述:当数据分析平台遇上Function Calling
如果你正在构建一个数据分析平台,或者负责一个需要处理大量非结构化数据查询的智能系统,那么“让机器理解人话”并自动执行复杂操作,可能是你当前面临的最大痛点。传统的BI工具需要用户学习复杂的查询语言(如SQL)和操作界面,而简单的自然语言查询又往往只能得到一些基础的统计结果,无法完成“从数据中提取洞察”到“基于洞察采取行动”的闭环。这正是我们这次要探讨的核心:如何通过整合Function Calling技术,将一个普通的AI数据分析助手,升级为一个真正能“动手干活”的智能Agent。
简单来说,这个项目的目标,就是打造一个能听懂人话、会思考、还能自动调用工具去执行的“数据分析专员”。用户不再需要说“帮我查一下上个月华东区的销售额,然后导出成Excel”,而是可以直接说:“分析一下上个月华东区的销售表现,找出下滑最严重的三个产品,并给对应的区域经理发一封预警邮件,附上详细数据对比。” 后者就是一个典型的Agent工作流:理解意图、规划步骤、调用函数(查询数据库、分析数据、发送邮件)并最终完成任务。
Function Calling(函数调用)是实现这一愿景的关键技术桥梁。它并不是指编程中的函数调用,而是大语言模型(LLM)的一种高级能力。当LLM接收到用户的自然语言指令时,它可以识别出其中隐含的“执行意图”,并结构化地输出一个或多个“函数调用请求”,其中包含了需要调用的函数名以及精确的参数。然后,我们的应用程序捕获这个请求,去真正执行对应的代码函数(比如执行一段SQL,或调用一个邮件API),并将执行结果返回给LLM。LLM再根据结果组织最终的自然语言回复给用户。这个过程,让LLM从一个“健谈的顾问”变成了一个“能干的执行者”。
2. 核心架构设计:构建一个可执行的智能体
一个整合了Function Calling的智能数据分析Agent,其架构远比一个简单的“问答机器人”要复杂。它需要清晰地划分责任,协调多个组件。下面这张架构图描绘了其核心工作流与组件交互:
整个系统可以划分为五个核心层次,自下而上分别是:
数据与工具层:这是Agent的“手”和“眼睛”。它包含了所有Agent可以调用的实际能力,例如:
- 数据库连接器:执行SQL查询,连接MySQL、PostgreSQL、数据仓库等。
- 数据分析引擎:调用Pandas、NumPy进行数据聚合、计算指标、趋势分析。
- 外部API客户端:调用企业内部或第三方API,如发送邮件(SMTP/邮件服务API)、生成图表(Matplotlib/Plotly)、更新CRM系统、触发工作流等。
- 文件操作系统:读写Excel、CSV文件,保存中间结果或最终报告。
Function Calling适配层:这是系统的“关节”,负责将抽象的“函数调用意图”转化为具体的“函数执行动作”。它包含两个关键部分:
- 函数注册表:一个中心化的清单,定义了每个可供调用的函数。每个定义包括:函数名、描述(供LLM理解用途)、参数列表(名称、类型、描述)。例如,一个名为
query_sales_data的函数,其描述可能是“查询指定时间段和区域的销售数据”,参数包括start_date,end_date,region。 - 函数执行器:根据LLM输出的调用请求(包含函数名和参数),在注册表中找到对应的真实函数(一段Python代码),传入参数并执行,然后捕获返回值或异常。
大语言模型(LLM)层:这是Agent的“大脑”。我们通过精心设计的系统提示词(System Prompt),告诉LLM它是一名数据分析专家,并且它拥有哪些可用的工具(即函数注册表中的信息)。当用户输入查询时,LLM的核心职责是进行意图识别与规划:判断是否需要调用函数、调用哪个函数、参数应该是什么。它输出的是结构化的调用指令,而不是最终答案。
会话与流程控制层:这是Agent的“小脑”和“记忆中枢”,负责管理复杂的多轮交互。因为一个复杂任务可能需要多次函数调用。例如,用户问“对比一下A产品和B产品的销量”,Agent可能需要先调用get_product_a_sales,再调用get_product_b_sales,最后调用compare_data。这个层需要维护会话状态,管理调用历史,并决定何时将函数执行结果返回给LLM进行下一轮思考,何时向用户输出最终结果。
用户交互层:这是Agent的“脸面”,可以是Web界面、聊天机器人窗口、API接口或命令行工具。它接收用户输入,展示Agent的思考过程(可选)和最终回复。
这个架构的优势在于解耦和灵活性。你可以轻松地增加新的工具(只需在注册表定义并实现函数),而无需修改核心的LLM交互逻辑。大脑(LLM)只负责决策,手(工具层)负责执行,各司其职。
3. 关键技术实现:从函数定义到执行闭环
理解了架构,我们深入到代码层面,看看如何实现一个完整的Function Calling闭环。这里我们以OpenAI的GPT系列模型为例,因为它提供了非常成熟的Function Calling接口。但原理同样适用于其他支持类似功能的模型。
3.1 定义你的工具函数集
首先,我们需要明确Agent能做什么。假设我们的数据分析平台主要有三个能力:查询数据、进行基础分析、发送报告。
# 模拟数据库查询函数 def query_sales_data(start_date: str, end_date: str, region: str = None) -> str: """ 模拟查询销售数据。 在实际应用中,这里会连接数据库执行SQL。 """ # 模拟SQL: SELECT * FROM sales WHERE date BETWEEN start_date AND end_date AND region=region return f"模拟数据:{region}地区在{start_date}到{end_date}期间的销售额为120万元,订单数450笔。" # 数据分析函数 def calculate_summary_statistics(data_description: str) -> str: """ 对查询到的数据进行基础统计分析。 """ # 在实际中,这里会接收真实数据对象,进行pandas操作 return f"基于数据 '{data_description[:50]}...' 的分析结果:平均客单价2667元,环比增长5%。" # 报告生成与发送函数 def send_analysis_report(to_email: str, subject: str, content: str) -> str: """ 模拟发送邮件报告。 """ # 在实际中,这里会调用SMTP或邮件服务API(如SendGrid, AWS SES) print(f"[模拟邮件发送] 至:{to_email}, 主题:{subject}") print(f"内容:{content[:100]}...") return f"分析报告已成功发送至 {to_email}。"3.2 为LLM创建函数描述
LLM需要知道这些函数的存在以及如何使用它们。我们需要按照OpenAI的格式,将函数描述成它能够理解的JSON Schema。
# 定义可供LLM调用的函数列表 tools = [ { "type": "function", "function": { "name": "query_sales_data", "description": "根据指定的时间范围和区域查询销售数据。", "parameters": { "type": "object", "properties": { "start_date": {"type": "string", "description": "开始日期,格式YYYY-MM-DD。"}, "end_date": {"type": "string", "description": "结束日期,格式YYYY-MM-DD。"}, "region": {"type": "string", "description": "区域名称,例如‘华东’、‘华北’。如果不指定,则查询所有区域。"} }, "required": ["start_date", "end_date"] } } }, { "type": "function", "function": { "name": "calculate_summary_statistics", "description": "对给定的数据集或数据描述进行基础统计分析,如计算平均值、总和、增长率等。", "parameters": { "type": "object", "properties": { "data_description": {"type": "string", "description": "需要分析的数据的描述或标识符。"} }, "required": ["data_description"] } } }, { "type": "function", "function": { "name": "send_analysis_report", "description": "将分析结果通过邮件发送给指定收件人。", "parameters": { "type": "object", "properties": { "to_email": {"type": "string", "description": "收件人的邮箱地址。"}, "subject": {"type": "string", "description": "邮件主题。"}, "content": {"type": "string", "description": "邮件的正文内容,通常包含分析摘要。"} }, "required": ["to_email", "subject", "content"] } } } ]关键点:
description字段至关重要。LLM完全依赖这个描述来决定是否以及何时调用该函数。描述要清晰、具体,说明函数的用途和适用场景。参数描述也要详细,这能极大提高LLM填充参数的准确性。
3.3 构建核心的Agent执行循环
这是最核心的部分,它实现了“用户输入 -> LLM思考 -> 函数执行 -> 回复用户”的循环。这个循环可能需要运行多次,直到LLM认为不需要再调用任何工具,可以给出最终答案为止。
import openai import json # 初始化OpenAI客户端(请替换为你的API Key) client = openai.OpenAI(api_key="your-api-key") def run_agent_conversation(user_query: str, system_prompt: str, tools: list, max_turns=5): """ 运行一个带Function Calling的Agent对话。 """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_query} ] for turn in range(max_turns): # 1. 调用LLM,传入当前消息历史和工具定义 response = client.chat.completions.create( model="gpt-4-turbo-preview", # 或 gpt-3.5-turbo messages=messages, tools=tools, tool_choice="auto", # 让模型自动决定是否调用函数 ) response_message = response.choices[0].message tool_calls = response_message.tool_calls # 2. 将LLM的回复添加到消息历史中 messages.append(response_message) # 3. 检查LLM是否想要调用函数 if not tool_calls: # 如果没有工具调用,说明LLM已经准备好最终回复 print("Agent 最终回复:", response_message.content) return response_message.content # 4. LLM要求调用函数,准备执行 available_functions = { "query_sales_data": query_sales_data, "calculate_summary_statistics": calculate_summary_statistics, "send_analysis_report": send_analysis_report, } # 遍历所有被请求的工具调用(LLM可能一次请求多个) for tool_call in tool_calls: function_name = tool_call.function.name function_to_call = available_functions.get(function_name) if function_to_call: # 解析LLM提供的参数 function_args = json.loads(tool_call.function.arguments) print(f"Agent 决定调用函数:{function_name}, 参数:{function_args}") # 执行真实的函数 function_response = function_to_call(**function_args) print(f"函数执行结果:{function_response}") # 5. 将函数执行结果作为新的消息追加到历史中,供LLM下一轮参考 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": str(function_response), # 结果必须是字符串 }) else: # 处理函数未找到的情况 error_msg = f"错误:函数 {function_name} 未找到或不可用。" print(error_msg) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": error_msg, }) print("达到最大对话轮数,可能任务未完成。") return "任务处理超时或遇到复杂循环。" # 系统提示词,定义Agent的角色和能力 system_prompt = """ 你是一个智能数据分析助手。你可以通过调用工具来查询数据、分析数据和发送报告。 请根据用户的问题,判断是否需要调用工具,以及调用哪些工具。 如果用户的问题需要多个步骤,请逐步调用工具。 在获得所有必要信息后,用清晰、友好的语言总结答案给用户。 """ # 启动一个对话示例 user_query = “帮我查一下今年第一季度华东区的销售情况,做个简单分析,然后把分析摘要发到我的邮箱 analyst@company.com。” final_answer = run_agent_conversation(user_query, system_prompt, tools)这个run_agent_conversation函数就是Agent的“引擎”。它通过循环,让LLM和外部工具持续交互,直到任务完成为止。tool_choice="auto"是关键参数,它授权模型自主决定何时调用函数。
4. 实战场景解析:复杂任务的多步拆解与执行
让我们通过一个更复杂的例子,看看Agent是如何像人类一样思考并一步步完成任务的。用户提出请求:“找出上个月销售额同比下降超过10%的所有产品,并给它们的产品经理生成一份包含详细数据和改进建议的Word报告。”
对于这个任务,一个经验丰富的数据分析师会在心里默默规划以下步骤:
- 获取基础数据:查询上个月和去年同月的产品级销售数据。
- 计算与分析:计算每个产品的同比变化率,筛选出下降超过10%的产品。
- 数据增强:获取这些产品的详细信息(如类别、负责人)。
- 报告生成:将筛选结果和详细信息整理成固定格式的报告文档。
- 交付:将报告保存为Word文件,并通知相关人员。
我们的Agent通过Function Calling,可以完美模拟这个过程。以下是它可能产生的内部对话流(简化版):
- 第一轮(用户输入):消息历史包含用户请求。
- 第二轮(LLM思考):LLM识别出需要先获取数据。它输出一个工具调用请求:
调用 query_product_sales(month=‘2024-03’, compare_to=‘2023-03’)。 - 第三轮(系统执行并反馈):函数执行器运行查询,返回数据集。系统将结果
“产品A上月销售额50万,去年同期55万;产品B上月30万,去年同期40万;...”作为tool角色消息追加。 - 第四轮(LLM再次思考):LLM收到数据后,意识到需要计算。它输出:
调用 calculate_yoy_decline(sales_data=[...], threshold=0.1)。 - 第五轮(执行与反馈):分析函数运行,返回
“符合条件的产品:A (下降9.1%,未达标),B (下降25%,达标),C (下降15%,达标)”。注意,这里LLM可能因为数据描述不清而犯错(产品A未达标却被列出),这体现了清晰数据接口的重要性。 - 第六轮(LLM思考):LLM现在有了目标产品列表[B, C]。它需要获取详细信息并生成报告。它可能输出两个并行的工具调用请求:
调用 get_product_details(product_ids=[‘B’, ‘C’])调用 generate_word_report(template=‘performance_alert’, data=analysis_result, details=product_details)
- 第七轮(执行与反馈):两个函数并行或顺序执行,分别返回产品详情和报告文件路径。
- 第八轮(LLM最终总结):LLM收到所有结果后,组织最终回复给用户:“已完成分析。发现产品B和C同比销售额下降超过10%。已根据‘performance_alert’模板生成了详细的Word报告,包含各产品数据及初步改进建议,报告保存在路径:/reports/alert_20240415.docx。报告中也附上了产品经理的联系信息。”
这个过程展示了Agent如何将模糊的自然语言指令,分解为一系列明确的、可执行的动作序列。每个动作(函数调用)都解决了整个任务的一个子问题,并且后一个动作可以依赖前一个动作的结果。
实操心得:设计函数时,粒度很重要。既不能太粗(如一个
do_analysis函数包办一切,导致LLM无法介入中间步骤),也不能太细(如把filter_data和sort_data分开,增加不必要的调用轮次)。好的函数设计应该对应一个清晰的、有独立价值的业务步骤。同时,为函数提供丰富、准确的上下文(通过参数和描述)是成功的关键。例如,generate_word_report函数如果有一个template参数,并明确说明可选模板及其用途,LLM就能更好地匹配用户意图。
5. 性能优化与高级技巧
当你的Agent从Demo走向生产环境,面对高并发和复杂任务时,以下几个方面的优化至关重要。
5.1 提示词工程:引导更精准的函数调用
系统提示词是Agent的“入职培训”。一份好的提示词能极大提升模型表现。
advanced_system_prompt = """ 你是一个专业的数据分析Agent,名为“DataWizard”。你的核心能力是使用一系列工具来获取、分析和呈现数据。 # 你的原则 1. 准确性优先:在不确定时,务必调用工具查询确认,而非猜测。 2. 分步执行:对于复杂任务,将其分解为多个顺序或并行的小步骤,一次只调用一个或一组逻辑相关的工具。 3. 信息完整:在调用需要参数的函数时,确保从对话历史或工具结果中提取了所有必需参数。如果用户未提供,主动询问。 4. 结果验证:在获得工具执行结果后,简要核验其是否合理,是否回答了当前子问题。 # 可用工具概述 你拥有以下工具,请根据问题选择使用: - 数据查询工具:用于从数据库或数据仓库获取原始数据。 - 数据分析工具:用于执行计算、统计、建模等分析操作。 - 可视化与报告工具:用于生成图表、报告文档。 - 通知与集成工具:用于通过邮件、消息通知结果,或更新其他业务系统。 # 输出格式 当你决定调用工具时,请严格按照工具定义的JSON格式输出。 当你直接回复用户时,请确保回复基于工具返回的事实,并做到清晰、有条理。 现在,请开始处理用户的请求。 """此外,在对话历史(messages)中,除了user,assistant,tool这三种角色,你还可以插入system角色的后续指令,或在关键时刻以user身份进行引导,这被称为“提示词注入”,可以用来纠正Agent的跑偏或推动停滞的任务。
5.2 处理复杂依赖与并行调用
有些任务步骤间有严格依赖(必须先A后B),有些则可以并行(A和B可同时进行)。LLM本身不擅长管理这种复杂流程,这就需要我们在流程控制层实现逻辑。
- 顺序执行:如上例所示,通过循环让LLM逐步决策,自然形成了顺序执行。这是最简单的方式。
- 并行执行:当LLM一次性返回多个
tool_calls时,我们可以利用多线程或异步IO来并发执行这些函数,最后将所有结果一次性收集并返回给LLM。这能显著减少任务总耗时。 - 有向无环图(DAG):对于极其复杂、步骤固定的工作流(如ETL管道),可以预先定义好一个DAG。LLM的角色可能退化为触发这个工作流,或者在工作流的某些决策节点提供输入。工作流引擎(如Apache Airflow, Prefect)负责调度和执行各个函数节点。
5.3 错误处理与稳定性保障
在生产环境中,任何环节都可能出错:LLM输出格式不对、函数执行异常、网络超时、参数无效等。鲁棒的Agent必须具备完善的错误处理机制。
- LLM输出解析异常:使用
try...except包裹json.loads(tool_call.function.arguments),如果解析失败,可以向对话历史中注入一条错误信息,如“你提供的参数格式有误,请检查并重新输出。”,然后让LLM重试。 - 函数执行异常:在执行真实函数时进行异常捕获,并将友好的错误信息(如“数据库连接失败,请稍后再试”)而非堆栈跟踪返回给LLM。LLM有时能根据错误信息调整策略。
- 重试与降级策略:对于暂时性失败(如网络抖动),可以设置重试机制。如果某个核心工具不可用,是否有备选方案?例如,发送邮件失败,是否可以先保存报告到本地?
- 超时控制:为每次LLM调用和函数执行设置超时时间,防止单个步骤卡死整个Agent。
- 输入验证与清理:在执行函数前,对LLM提供的参数进行验证和清理,防止SQL注入、路径遍历等安全问题。永远不要盲目信任LLM的输出。
5.4 成本与延迟优化
频繁调用LLM和大型模型会产生可观成本与延迟。
- 模型选型:对于工具调用决策(即判断该调用哪个函数),使用较小的、快速且便宜的模型(如GPT-3.5-Turbo)可能就足够了。只有在需要复杂推理或生成最终总结时,才使用更强大的模型(如GPT-4)。
- 上下文管理:对话历史会随着轮次增长,导致每次调用LLM的token数增加,成本上升、速度变慢。需要定期“总结”或“修剪”历史。例如,可以将过去多轮的函数调用和结果,总结成一条精简的“背景摘要”插入到新的系统提示中,然后清空旧的历史。
- 缓存:对于相同的用户查询和参数,函数调用结果可以缓存起来,避免重复计算或查询。这在数据看板类应用中效果显著。
- 流式输出:对于最终给用户的文本回复,可以使用流式响应(streaming)来提升用户体验,让用户尽快看到部分结果。
6. 常见问题与排查指南
在实际开发和运维中,你肯定会遇到各种各样的问题。下面这个表格整理了一些典型问题及其排查思路。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| LLM始终不调用函数,直接回答 | 1. 系统提示词未明确要求使用工具。 2. 函数描述太模糊,LLM不理解何时使用。 3. 用户问题太简单,LLM认为无需工具。 | 1. 检查并强化系统提示词,明确其“助手”角色和工具使用职责。 2. 重写函数描述,使其更具体、场景化。例如,将“处理数据”改为“查询指定时间段的销售订单数据”。 3. 在 tools参数中,尝试将tool_choice设置为“required”或指定具体函数名,强制模型使用工具。 |
| LLM调用了错误的函数,或参数填错 | 1. 函数间功能描述重叠,区分度不够。 2. 参数描述不清,LLM无法准确映射。 3. 对话历史上下文误导。 | 1. 审查所有函数描述,确保它们各有明确、独特的职责范围。 2. 细化参数描述,提供示例值。例如, region参数描述可加上“例如:‘华东’、‘华北’, 请使用系统中定义的区域全称”。3. 观察对话历史,看是否是之前LLM或用户的错误信息导致了误解。可以考虑在关键步骤后重置或精简历史。 |
| 函数执行成功,但LLM无法理解结果 | 1. 函数返回的结果过于复杂(如大型JSON、二进制数据),LLM难以解析。 2. 返回结果格式不友好。 | 1.永远让函数返回字符串。对于复杂对象,将其转换为清晰、简洁的文本摘要。例如,返回“查询到125条记录,总销售额500万。详情:产品A:200万,产品B:150万...”而不是原始的125行数据。 2. 设计结构化的返回格式,如“状态:成功/失败;数据:...;消息:...”。LLM对结构化的文本理解更好。 |
| 多轮对话后,Agent“失忆”或逻辑混乱 | 上下文长度超出模型限制,或无关历史信息干扰了当前决策。 | 1. 实施上下文窗口管理。只保留最近N轮对话,或将早期的长篇对话总结成一段摘要。 2. 在任务边界(如用户开启一个新问题)时,主动清空对话历史,或插入系统指令重置上下文。 |
| 处理长耗时任务时,请求超时 | 函数执行时间过长(如跑一个复杂查询),导致HTTP请求超时。 | 1. 将同步调用改为异步处理。接收到用户请求后立即返回一个“任务已接收”的响应,后台执行Agent工作流,通过WebSocket、轮询或回调通知用户结果。 2. 优化底层函数性能,如为数据库查询添加索引。 |
| 安全性问题:LLM被诱导执行危险函数 | 函数设计不当,暴露了危险操作(如delete_database,send_email无限制)。 | 1.遵循最小权限原则。Agent只能调用必要的、安全等级明确的函数。 2.实施用户权限校验。在函数执行内部,根据当前会话用户身份校验其是否有权执行此操作。 3.对参数进行严格过滤和校验,特别是涉及文件路径、系统命令、SQL语句的部分。 4. 考虑设置一个“安全层”或“审批层”,对于高风险操作,先生成预览,需用户确认后再执行。 |
7. 超越基础:Agent的进化方向
将Function Calling整合进数据分析平台,只是构建智能Agent的第一步。要让这个Agent变得更强大、更可靠、更智能,还有很长的路要走。这里分享几个我认为值得投入的进化方向。
记忆与个性化:目前的Agent通常是“无状态”的,每次对话相互独立。我们可以为Agent引入长期记忆。例如,将对话历史、用户偏好、常用查询模式向量化后存入数据库(如Pinecone, Weaviate)。当新对话开始时,先检索相关的历史记忆作为上下文注入,这样Agent就能记住“张经理通常关心华东区的KPI”,或者“上周已经分析过这个异常,结论是…”,提供连续、个性化的服务。
工具学习与扩展:手动编写和维护所有工具函数会随着业务增长变得繁琐。可以探索让Agent自己学习使用新工具。例如,提供一个“工具使用说明书”的检索库,当用户提出新需求时,Agent先检索是否有类似功能的工具描述,然后尝试根据描述来调用。更进一步,可以结合代码生成能力,让Agent为一些简单、重复的任务动态生成并执行代码片段作为临时工具。
验证与确认机制:对于涉及数据修改、发送通知或产生费用的关键操作,不能让Agent完全自主。需要在流程中设计“人工确认点”或“复核点”。例如,Agent在准备发送一份给重要客户的报告前,可以生成报告预览并询问:“这是为您生成的报告草稿,请确认内容无误后回复‘发送’。” 这平衡了自动化与风险控制。
多模态能力扩展:数据分析的结果不仅仅是数字和文字,图表往往更直观。我们可以为Agent集成图像生成或图表生成函数。用户可以说“把刚才的销售趋势做成折线图”,Agent调用图表生成函数,将图片返回或插入报告。更进一步,如果平台支持上传数据文件,Agent甚至可以具备“视觉”能力,通过多模态模型理解图片、表格截图中的信息进行分析。
评估与持续改进:如何衡量Agent的表现?需要建立一套评估体系。包括:任务完成率、函数调用准确率、用户满意度、平均对话轮次等。可以通过A/B测试不同的提示词、函数描述,或收集失败案例进行针对性优化。这是一个持续迭代的过程。
整合Function Calling来强化Agent,本质上是为数据分析能力装上了“可编程”的接口。它打破了传统自动化脚本的僵化和自然语言交互的无力感,在灵活性与执行力之间找到了一个美妙的平衡点。从我自己的实践来看,最大的挑战往往不在技术实现,而在于如何精准地定义业务场景、拆解任务步骤,并设计出那一组恰到好处的“工具函数”。这需要开发者和业务专家紧密合作。当你看到用户用一句简单的话,就驱动系统完成了一系列曾经需要多次点击、编写代码才能完成的工作时,那种价值感是非常直接的。这个领域还在快速演进,新的框架和模式不断涌现,但核心思想是不变的:让机器更懂你,并为你做得更多。