引言:Excel表格里的AI革命
如果我告诉你,未来的Excel公式不再是冷冰冰的=SUM(A1:A10)
,而是温暖人心的=RunAgent("帮我调查这家公司的员工数")
,你会不会觉得科幻小说照进了现实?
在这个AI狂飙突进的时代,我们见证了太多"不可能"变成"理所当然"。但当我第一次看到ExcelAgentTemplate这个项目时,还是被它的巧妙构思惊艳到了------它不是简单地把GPT套个壳子,而是真正理解了Excel用户的痛点,用一种近乎魔法的方式,让AI Agent成为了Excel的原生函数。
想象一下这样的场景:你手里有一份包含500家企业名称的Excel表格,老板要你在三天内调查每家公司的员工数、注册地址和主营业务。传统做法?打开浏览器,一家一家搜索,复制粘贴到天荒地老。而有了ExcelAgentTemplate,你只需要在B列输入=RunAgent("调查" & A2 & "的员工数")
,然后下拉填充,剩下的时间去喝杯咖啡------AI会自动帮你完成所有调查工作。
这不是科幻,这是ExcelAgentTemplate带来的生产力革命。今天,我们就来深度剖析这个项目的技术架构、实现细节和应用场景,看看它是如何用Python + C# + LangChain + Excel-DNA这套组合拳,打造出一个优雅而强大的AI Agent系统的。
一、项目概览:Excel与AI的完美联姻
1.1 核心价值主张
ExcelAgentTemplate的本质,是一个跨语言、跨平台的分布式AI Agent调用系统。它的核心价值在于:
-
零门槛AI能力:不需要学Python,不需要懂API,Excel用户用最熟悉的公式语法就能调用最先进的大语言模型。
-
真正的自动化:不是简单的问答,而是能够自主执行网络搜索、信息提取、数据分析等复杂任务的Agent。
-
生产级架构:采用前后端分离、异步处理、缓存优化等专业设计,而非玩具级的Demo。
-
可扩展性:基于FastAPI和LangChain,可以轻松扩展新的Agent能力和工具。
1.2 技术栈一览
看看这个项目的技术选型,简直是一堂"如何选择合适技术栈"的教科书级示范:
后端(Python侧):
-
FastAPI:高性能的异步Web框架,自动生成OpenAPI文档
-
LangChain:LLM应用开发框架,提供Agent、Tool、Chain等抽象
-
LangChain-OpenAI:OpenAI模型集成
-
Tavily:专为LLM优化的网络搜索工具(比直接用Google API更聪明)
-
Joblib:用于整个Chain的缓存(LangChain自带缓存只能缓存LLM调用)
-
Uvicorn:ASGI服务器,支持高并发异步请求
前端(Excel侧):
-
Excel-DNA:.NET平台的Excel Add-In开发框架,神器级存在
-
C# .NET 4.7.1:稳定的运行时环境
-
Newtonsoft.Json:JSON序列化/反序列化
-
HttpClient:异步HTTP请求客户端
这个技术栈的精妙之处在于:
-
Python负责AI能力(LangChain生态成熟)
-
C#负责Excel集成(Excel-DNA专业稳定)
-
HTTP API作为中间桥梁(解耦、可扩展)
-
各自发挥所长,没有为了技术统一而硬凑
1.3 架构设计哲学
ExcelAgentTemplate的架构体现了几个重要的设计哲学:
1. 关注点分离(Separation of Concerns)
┌─────────────┐ HTTP/JSON ┌──────────────┐
│ Excel │ ───────────────────> │ FastAPI │
│ (C# Add-In)│ <─────────────────── │ (Python) │
└─────────────┘ └──────────────┘
│ │
│ Excel-DNA │ LangChain
│ 异步处理 │ Agent Executor
│ │
v v
用户界面 AI能力层
Excel端只负责UI交互和异步任务管理,完全不关心AI怎么实现;Python端只负责AI逻辑,完全不关心Excel怎么调用。这种设计让两端都可以独立演进。
2. 异步优先(Async by Default)
无论是Python的async/await
,还是C#的Task
和Excel-DNA的异步函数注册,整个调用链路都是异步的。这确保了:
-
Excel不会因为等待AI响应而卡死
-
可以并行处理多个单元格的请求
-
用户体验丝滑流畅
3. 缓存至上(Cache is King)
项目用Joblib实现了整个Agent执行链路的缓存。这意味着同样的问题第二次问,几乎是秒回。对于Excel这种经常需要重新计算的场景,缓存能节省大量API调用费用和等待时间。
4. 约定优于配置(Convention over Configuration)
默认值设计得非常合理:
-
模型默认
gpt-4-turbo-preview
(性能与成本的平衡点) -
服务器默认
http://localhost:8889/chat
(本地开发最常见) -
搜索结果默认10条(足够全面又不过载)
-
超时时间10分钟(足够长但不至于无限等待)
用户在80%的场景下,只需要传入一个参数(提示词),其他都有合理默认值。
二、后端架构:Python + FastAPI + LangChain的黄金组合
2.1 FastAPI服务:简洁而专业
让我们从langchain_fastapi.py
的代码结构说起。这个文件虽然只有134行,但设计得相当专业。
2.1.1 配置管理:可扩展的常量设计
DEFAULT_PORT: int = 8889
DEFAULT_HOST: str = "0.0.0.0"
DEFAULT_LOG_LEVEL: str = "debug"
DEFAULT_CACHE_DIR: str = "./.cache"
这些常量不是硬编码在代码里,而是作为命令行参数的默认值。启动服务时可以这样覆盖:
python langchain_fastapi.py --host 0.0.0.0 --port 9000 --log_level info
这种设计让部署变得极其灵活:开发环境用默认值,生产环境通过启动脚本传参,甚至可以用环境变量或配置文件进一步扩展。
2.1.2 缓存策略:Joblib的巧妙应用
whole_chain_cache_memory = Memory(DEFAULT_CACHE_DIR)
@whole_chain_cache_memory.cache
async def chat_internal(chat_input: ChatInput):
# ... Agent执行逻辑
这里有个细节值得品味:为什么不用LangChain自带的缓存机制?
作者在注释里解释得很清楚:
# 实装メモ:
# LangChain の Caching 機能は LLM の入出力はキャッシュできても、
# Chain 全体はキャッシュできません。
LangChain的缓存只能缓存单个LLM调用,但一个Agent的执行过程可能包括:
-
解析用户意图
-
决定使用哪个工具
-
调用搜索引擎
-
提取搜索结果
-
生成最终答案
每一步都可能有LLM调用,如果只缓存单步,还是会有很多重复计算。而Joblib缓存整个函数的输入输出,只要chat_input
一样,直接返回之前的结果,效率高出一个数量级。
当然,这种缓存策略也有trade-off:如果你希望每次都得到不同的答案(比如创意生成场景),就需要调整缓存策略。但对于事实查询类任务,这种缓存是完美的。
2.2 Pydantic模型:类型安全的API契约
class ChatInput(BaseModel):
message: str = Field(
description="ユーザーからのメッセージです。空文字は許可されません。")
model: str = Field(
"gpt-4-turbo-preview",
description="使用するモデルの名前です。デフォルトは 'gpt-4-turbo-preview' です。")
Pydantic是FastAPI的灵魂伴侣。这个模型定义做了三件事:
-
自动验证 :如果客户端传来的JSON缺少
message
字段或类型不对,FastAPI会自动返回400错误,并附上详细的错误信息。你不需要写任何if not message
这样的代码。 -
自动文档 :访问
http://localhost:8889/docs
,FastAPI会生成交互式的API文档,Field
里的description
会显示在文档里。这对于团队协作或开源项目至关重要。 -
类型提示 :IDE可以根据这个模型提供智能提示。写
chat_input.
的时候,IDE会自动补全message
和model
。
这种"一次定义,多处受益"的设计,是现代Python开发的最佳实践。
2.3 LangChain Agent:可插拔的工具系统
2.3.1 工具配置:Tavily搜索引擎
web_search_tools = [
TavilySearchResults(
description=(
"A search engine optimized for comprehensive, accurate, and trusted results. "
"Useful for when you need to answer questions about current events. "
"Input should be a search query. "
"If you don't get good search results, "
"please change the keywords and search again."
),
max_results=10,
verbose=True),
]
这里的description
不是给人看的,是给LLM看的!Agent在决定使用哪个工具时,会把这个描述作为工具的"说明书"。所以这个描述写得好不好,直接影响Agent的智能程度。
比如这里特别加了一句:
"If you don't get good search results, please change the keywords and search again."
这是在教Agent"如果第一次搜索不理想,换个关键词再试试"。这种Prompt Engineering技巧,是让Agent更智能的关键。
作者还特别选择了Tavily而不是SerpAPI或DuckDuckGo。为什么?README里有解释:
Tavily は、素の Google 検索や DuckDuckGo、Bing と比べて LLM との相性が良いと印象です。
Tavily的搜索结果经过优化,更适合LLM处理。它会提取页面的核心内容,去掉广告和无关信息,让LLM更容易找到答案。这就是"为AI优化的工具"和"为人类优化的工具"的区别。
2.3.2 Agent创建:OpenAI Tools Agent
agent = create_openai_tools_agent(
llm=ChatOpenAI(model=chat_input.model),
tools=tools,
prompt=hub.pull("hwchase17/openai-tools-agent")
)
这三行代码信息量巨大:
-
LLM可配置 :
model=chat_input.model
意味着用户可以在Excel里指定用GPT-4、GPT-3.5还是其他模型。这对成本控制很重要------简单任务用便宜的模型,复杂任务用强大的模型。 -
工具可扩展 :
tools=tools
是个列表,你可以轻松添加新工具。比如想加个"查询数据库"的工具:from langchain.tools import Tool database_tool = Tool( name="QueryDatabase", func=query_database_function, description="Query the company database to get detailed information." ) tools = web_search_tools + [database_tool]
-
Prompt来自LangChain Hub :
hub.pull("hwchase17/openai-tools-agent")
从云端拉取一个经过优化的Prompt模板。这个模板是LangChain团队精心调教的,比自己瞎写强多了。而且如果LangChain更新了更好的Prompt,你不需要改代码,重启一下就能用上新版本。
2.3.3 AgentExecutor:执行引擎
chain = AgentExecutor(
agent=agent,
tools=tools,
handle_parsing_errors=True,
max_iterations=30,
verbose=True
)
AgentExecutor
是Agent的运行时环境,这里的参数设置展现了作者的经验:
-
handle_parsing_errors=True:LLM有时会输出格式不对的内容,这个参数让系统自动重试,而不是直接崩溃。生产环境必备。
-
max_iterations=30:防止Agent进入死循环。有些复杂任务可能需要Agent"思考-行动-观察"多轮,但也不能让它无限循环下去。30次迭代是个经验值。
-
verbose=True:开发阶段非常有用,可以看到Agent的思考过程。生产环境可以关掉以提高性能。
2.4 异步处理:性能的关键
result = await chain.ainvoke({"input": chat_input.message})
注意这里用的是ainvoke
(异步版本)而不是invoke
。这一个字母之差,性能差异天壤之别。
同步版本的问题:
# 假设每个请求需要10秒
# 100个并发请求 = 1000秒 = 16分钟
result = chain.invoke({"input": message})
异步版本的优势:
# 100个并发请求可以同时处理
# 总时间 ≈ 10秒(受限于OpenAI API的速率限制)
result = await chain.ainvoke({"input": message})
对于Excel场景,用户可能同时触发几十个单元格的计算。如果用同步处理,后面的请求要排队等待,用户体验很差。异步处理让所有请求几乎同时开始,大大缩短总时间。
2.5 环境配置:.env文件的最佳实践
from dotenv import load_dotenv
load_dotenv()
这两行代码看似简单,背后是现代应用开发的安全理念:敏感信息永远不写在代码里。
项目根目录应该有个.env
文件(不提交到Git):
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TAVILY_API_KEY=tvly-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
LangChain会自动读取这些环境变量。这样做的好处:
-
代码可以开源,不用担心泄露API密钥
-
不同环境(开发/测试/生产)可以用不同的密钥
-
团队成员各自用自己的密钥,互不干扰
三、前端架构:C# + Excel-DNA的魔法
如果说Python后端是大脑,那C#前端就是手脚------它负责把AI的能力"嫁接"到Excel这个庞大的身体上。
3.1 Excel-DNA:被低估的神器
Excel-DNA是.NET平台上开发Excel Add-In的开源框架,它的强大之处在于:
-
零依赖部署 :生成的
.xll
文件是自包含的,用户双击就能用,不需要安装.NET Framework或其他运行时(已包含在xll里)。 -
性能优异:用C#编写,直接调用Excel的C API,性能远超VBA和VSTO。
-
异步支持:原生支持异步函数,这对于需要等待网络请求的场景至关重要。
-
IntelliSense友好 :用户在Excel里输入
=RunAgent(
时,会自动显示参数提示和说明,体验和内置函数一模一样。
3.2 Add-In生命周期管理
public class AddIn : IExcelAddIn
{
public void AutoOpen()
{
RegisterFunctions();
}
public void AutoClose()
{
}
}
这个接口定义了Add-In的生命周期:
-
AutoOpen:Excel加载Add-In时调用,用于注册自定义函数
-
AutoClose:Excel卸载Add-In时调用,用于清理资源
目前AutoClose
是空的,但在实际项目中,你可能需要在这里:
-
关闭HTTP连接池
-
保存用户设置
-
清理临时文件
3.3 参数转换配置:处理Excel的奇葩类型
var paramConversionConfig = new ParameterConversionConfiguration()
.AddParameterConversion(ParameterConversions.GetOptionalConversion(treatEmptyAsMissing: true))
.AddNullableConversion(treatEmptyAsMissing: true, treatNAErrorAsMissing: true);
这段代码解决了一个很实际的问题:Excel的类型系统和C#不一样。
-
Excel的空单元格在C#里可能是
null
、""
或ExcelEmpty
-
Excel的错误值(
#N/A
、#VALUE!
等)需要特殊处理 -
Excel的可选参数和C#的可选参数语义不同
这个配置告诉Excel-DNA:
-
空单元格当作"未提供参数"处理
-
#N/A
错误也当作"未提供参数"处理
这样,用户在Excel里可以这样用:
=RunAgent(A1) ' A1如果是空的,函数会优雅地处理,而不是报错
3.4 异步函数:Excel不卡顿的秘密
[ExcelFunction(Name = "RunAgent", Description = "Excel から AI エージェントを非同期に実行します。")]
public static object RunAgent(
[ExcelArgument(Name = "inputMessage")] string inputMessage,
[ExcelArgument(Name = "model")] string model = "gpt-4-turbo-preview",
[ExcelArgument(Name = "serverUrl")] string serverUrl = "http://localhost:8889/chat"
)
{
return AsyncTaskUtil.RunTask(
"RunAgent",
new object[] { inputMessage, serverUrl, model },
async () => {
return await RunAgentAsync(inputMessage, model, serverUrl);
});
}
这里的AsyncTaskUtil.RunTask
是Excel-DNA提供的异步处理工具。它做了这些事:
-
**立即返回
#N/A
**:函数被调用时,立即返回#N/A
(表示"正在计算中"),Excel不会卡住。 -
后台执行:在后台线程执行真正的异步逻辑(发HTTP请求)。
-
自动更新:任务完成后,自动触发Excel重新计算该单元格,显示真正的结果。
这种模式让用户体验非常流畅:
-
输入公式 → 按回车 → 立即显示
#N/A
(0.1秒) -
后台处理 → 几秒或几十秒后 → 单元格自动更新为结果
对比一下同步函数:
- 输入公式 → 按回车 → Excel卡死 → 几十秒后 → 显示结果(期间鼠标变成加载图标,什么都不能做)
3.5 HTTP通信:JSON序列化的陷阱与处理
private static string ConvertExcelStringToJsonString(string text)
{
text = text.Replace("\\", "\\\\");
text = text.Replace("\"", "\\\"");
text = text.Replace("\b", "\\b");
text = text.Replace("\r", "\\r");
text = text.Replace("\n", "\\n");
text = text.Replace("\t", "\\t");
text = text.Replace("\f", "\\f");
return text;
}
这个函数看起来是在重复造轮子(明明有JsonConvert.SerializeObject
),但实际上是在处理一个微妙的边界情况。
Excel单元格里的文本可能包含各种特殊字符,特别是当用户从其他地方复制粘贴时。直接用JsonConvert.SerializeObject
有时会出现编码问题。这个手动转义的方法更可控,确保任何Excel里的文本都能安全地传输到Python后端。
对应的,还有一个反向转换:
private static string ConvertJsonStringToExcelString(string response)
{
response = response.Replace("\\\\", "\\");
response = response.Replace("\\\"", "\"");
// ... 其他转义
return response;
}
这对函数体现了一个重要原则:在系统边界处,永远不要相信外部数据的格式。即使是看起来简单的字符串传输,也要做好防御性编程。
3.6 错误处理:优雅降级
private static async Task<string> RunAgentAsync(string inputMessage, string model, string serverUrl)
{
try
{
HttpClient client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(1000 * 60 * 10); // 10分钟超时
var postData = JsonConvert.SerializeObject(new {
message = ConvertExcelStringToJsonString(inputMessage),
model = model
});
var content = new StringContent(postData, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync(serverUrl, content);
var responseString = await response.Content.ReadAsStringAsync();
return RemoveQuotes(ConvertJsonStringToExcelString(responseString));
}
catch (Exception ex)
{
return "Error: " + ex.Message;
}
}
这里的错误处理策略很聪明:不抛出异常,而是返回错误信息字符串。
为什么?因为Excel函数的调用者是普通用户,不是程序员。如果抛异常,Excel会显示一个难看的#VALUE!
错误,用户完全不知道发生了什么。而返回"Error: Connection timeout"
这样的字符串,用户至少知道是网络问题,可以检查后端服务是否启动。
这就是"为终端用户设计"和"为开发者设计"的区别。好的用户界面会把技术细节翻译成人类语言。
另一个值得注意的细节:
client.Timeout = TimeSpan.FromSeconds(1000 * 60 * 10); // 10分钟
为什么设置这么长的超时?因为Agent执行可能很慢:
-
需要多轮搜索(每次搜索3-5秒)
-
LLM推理需要时间(特别是GPT-4)
-
可能需要访问多个网页
如果设置30秒超时,很多复杂任务还没完成就被中断了。10分钟是个安全边界------足够长但不至于永远等待。
3.7 NuGet包管理:依赖的最小化艺术
看看packages.config
:
<package id="ExcelDna.AddIn" version="1.7.0" />
<package id="ExcelDna.Integration" version="1.7.0" />
<package id="ExcelDna.IntelliSense" version="1.7.0" />
<package id="ExcelDna.Registration" version="1.7.0" />
<package id="Newtonsoft.Json" version="13.0.3" />
<package id="System.Net.Http" version="4.3.4" />
核心依赖只有两个:
-
Excel-DNA系列:必需的Excel集成框架
-
Newtonsoft.Json :JSON处理(虽然.NET自带
System.Text.Json
,但Newtonsoft.Json
更成熟)
其他的System.*
包是为了解决.NET Framework版本兼容性问题。这种依赖管理策略体现了"少即是多"的哲学:
-
依赖越少,打包后的文件越小
-
依赖越少,版本冲突的风险越低
-
依赖越少,维护成本越低
四、通信协议:前后端的语言
前后端通过HTTP JSON API通信,协议设计得非常简洁:
4.1 请求格式
POST /chat
Content-Type: application/json
{
"message": "请调查微软公司的员工数",
"model": "gpt-4-turbo-preview"
}
4.2 响应格式
"根据最新的公开信息,截至2023年,微软公司拥有约221,000名员工。"
注意响应不是JSON对象,而是直接返回字符串。这个设计有点反直觉(通常我们会返回{"result": "..."}
),但对于这个场景是合理的:
-
简化C#端解析:直接读取响应体即可,不需要反序列化JSON对象再提取字段。
-
节省带宽:少了JSON对象的包装,响应体更小(虽然在这个场景下差别不大)。
-
语义清晰:这个API的职责就是"输入问题,输出答案",不需要额外的元数据。
如果未来需要返回更多信息(比如Token用量、执行时间等),可以改成:
{
"answer": "...",
"metadata": {
"tokens_used": 1500,
"execution_time": 12.5,
"model": "gpt-4-turbo-preview"
}
}
但在MVP阶段,简单就是美。
4.3 错误处理:HTTP状态码 vs 业务错误
有个有趣的设计选择:即使Agent执行失败,HTTP响应码仍然是200。
@app.post("/chat")
async def chat(chat_input: ChatInput):
result = await chat_internal(chat_input)
return result # 即使result是空字符串,也返回200
这和RESTful API的常见做法不同(通常失败会返回4xx或5xx)。但对于这个场景是合理的:
-
Agent失败不等于HTTP请求失败:网络通畅,服务器正常响应,只是LLM没能生成满意的答案,这不是HTTP层面的错误。
-
错误信息更有价值:返回200 + 错误信息字符串,比返回500 + 空响应体,对调试更有帮助。
-
简化客户端逻辑:C#端不需要判断状态码,只需要检查返回字符串是否以"Error:"开头。
这种设计体现了"协议为应用服务"的理念,而不是教条地遵循REST规范。
五、应用场景:从理论到实战
5.1 企业信息调研:传统Excel的噩梦,AI的天堂
场景描述: 你有一份500家公司的名单,需要调查每家公司的:
-
员工人数
-
注册地址
-
主营业务
-
最新融资情况
传统做法:
500家公司 × 4项信息 × 3分钟/项 = 6,000分钟 = 100小时 = 12.5个工作日
还不算复制粘贴出错、格式不一致、信息过时等问题。
ExcelAgentTemplate做法:
A列:公司名 | B列:员工数 | C列:地址 | D列:主营业务 | E列:融资情况 |
---|---|---|---|---|
微软 | =RunAgent("调查"&A2&"的员工数,只返回数字") |
=RunAgent("调查"&A2&"的注册地址") |
=RunAgent("调查"&A2&"的主营业务,50字以内") |
=RunAgent("调查"&A2&"的最新融资情况") |
下拉填充500行,然后去喝咖啡。20-30分钟后回来,所有信息已经填好。
为什么快?
-
并行处理:500个请求同时发送,不是串行执行
-
AI优化的搜索:Tavily直接返回结构化信息,不需要人工筛选
-
缓存机制:如果有重复的公司名,第二次是秒回
-
自动重试:搜索结果不理想时,Agent会自动换关键词重试
5.2 数据清洗与标准化:让AI做"脏活累活"
场景描述: 客户提供的数据表中,地址字段格式混乱:
-
"北京市海淀区中关村大街1号"
-
"中关村大街1号,海淀,北京"
-
"Beijing, Haidian District, Zhongguancun Street No.1"
需要统一成"省份 | 城市 | 区县 | 街道"的格式。
ExcelAgentTemplate做法:
=RunAgent("将以下地址标准化为'省份|城市|区县|街道'的格式,用竖线分隔:" & A2)
AI会理解语义,不管输入是中文、英文还是混乱的格式,都能输出统一的标准格式。这比写正则表达式或复杂的VBA脚本轻松多了。
5.3 翻译与本地化:专业术语不在话下
场景描述: 产品说明书需要翻译成10种语言,包含大量专业术语。
传统做法: 找翻译公司,等待几天,费用高昂,术语翻译可能不一致。
ExcelAgentTemplate做法:
A列:原文(中文) | B列:英文 | C列:日文 | D列:德文 |
---|---|---|---|
锂电池的能量密度为250Wh/kg | =RunAgent("将以下内容翻译成英文,保持专业术语的准确性:" & A2) |
=RunAgent("翻译成日文:" & A2) |
=RunAgent("翻译成德文:" & A2) |
GPT-4的翻译质量已经接近专业翻译,特别是对于技术文档。人工只需要做最后的审校,效率提升10倍。
5.4 舆情分析:从文本到洞察
场景描述: 收集了1000条用户评论,需要分析情感倾向和主要问题点。
ExcelAgentTemplate做法:
A列:用户评论 | B列:情感分析 | C列:问题分类 | D列:优先级 |
---|---|---|---|
"这个产品设计很好,但电池续航太差了!" | =RunAgent("分析以下评论的情感(正面/中性/负面):" & A2) |
=RunAgent("提取评论中提到的主要问题:" & A2) |
=RunAgent("评估问题的严重程度(高/中/低):" & C2) |
结合Excel的数据透视表功能,可以快速生成:
-
情感分布图
-
高频问题排行
-
优先修复列表
5.5 内容生成:批量创作的利器
场景描述: 电商平台需要为1000个商品生成SEO优化的描述。
ExcelAgentTemplate做法:
A列:商品名 | B列:规格 | C列:特点 | D列:SEO描述 |
---|---|---|---|
无线蓝牙耳机 | 蓝牙5.0/续航20h | 降噪/防水IPX7 | =RunAgent("为以下产品生成100字的SEO优化描述,包含关键词'蓝牙耳机'、'降噪'、'长续航':产品名:"&A2&",规格:"&B2&",特点:"&C2) |
AI会生成多样化的描述,避免重复,同时确保关键词密度合理。
5.6 智能提示与验证:Excel的"智能助手"
场景描述: 财务报表中,需要检查费用类型是否填写规范。
ExcelAgentTemplate做法:
A列:费用描述 | B列:建议的标准类型 | C列:置信度 |
---|---|---|
"请客户吃饭" | =RunAgent("将以下费用归类到标准费用类型(差旅费/招待费/办公费/其他):" & A2) |
=RunAgent("评估上一次归类的置信度(高/中/低):原始描述:" & A2 & ",归类结果:" & B2) |
这样可以实现"AI辅助+人工确认"的工作流:
-
AI给出建议
-
人工看置信度
-
高置信度的直接采纳,低置信度的仔细审查
六、性能优化:让系统飞起来
6.1 缓存策略:三层缓存体系
ExcelAgentTemplate虽然只显式实现了一层缓存(Joblib),但实际上有三层缓存在工作:
第一层:Joblib缓存(应用层)
@whole_chain_cache_memory.cache
async def chat_internal(chat_input: ChatInput):
-
缓存位置:本地文件系统(
./.cache
目录) -
缓存粒度:整个Agent执行结果
-
缓存键:
ChatInput
对象(message + model) -
命中率:对于重复查询,命中率极高(100%)
第二层:LangChain缓存(框架层) 虽然代码中没有显式启用,但可以通过配置开启:
from langchain.cache import SQLiteCache
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")
-
缓存位置:SQLite数据库
-
缓存粒度:单次LLM调用
-
好处:即使问题措辞不同但语义相同,也可能命中缓存
第三层:OpenAI缓存(服务层) OpenAI最近推出了Prompt Caching功能:
-
对于长提示词(如系统提示),OpenAI会自动缓存
-
价格降低50%,延迟降低80%
-
对Agent场景特别有用(系统提示通常很长)
优化建议:
# 为不同场景设置不同的缓存过期时间
from joblib import Memory
import os
cache_dir = os.getenv("CACHE_DIR", "./.cache")
cache_expiry = int(os.getenv("CACHE_EXPIRY_HOURS", 24))
memory = Memory(cache_dir, verbose=0)
@memory.cache(cache_validation_callback=lambda metadata:
(time.time() - metadata['created']) < cache_expiry * 3600)
async def chat_internal(chat_input: ChatInput):
# ...
这样可以:
-
事实查询:缓存24小时(事实不会快速变化)
-
实时数据:缓存1小时(如股票价格)
-
创意生成:不缓存(每次都要新鲜的创意)
6.2 并发控制:防止API速率限制
OpenAI有速率限制(如GPT-4:500 RPM),Excel用户同时触发100个单元格计算时,可能会触发限制。
解决方案:添加速率限制器
from asyncio import Semaphore
# 最多同时执行10个请求
rate_limiter = Semaphore(10)
async def chat_internal(chat_input: ChatInput):
async with rate_limiter:
# 原有逻辑
pass
这样即使用户触发1000个单元格,也会以每批10个的速度逐步处理,不会触发OpenAI的封禁。
6.3 Excel端优化:避免不必要的重新计算
Excel的自动重算机制可能导致性能问题:
问题场景:
' A1单元格
=TODAY()
' B1单元格
=RunAgent("今天是" & TEXT(A1, "yyyy-mm-dd") & ",请告诉我今天的新闻")
每次打开工作簿,TODAY()
会变化,导致B1重新计算,触发一次昂贵的Agent调用。
解决方案:使用智能缓存键
修改C#端,添加缓存键参数:
[ExcelFunction(Name = "RunAgent")]
public static object RunAgent(
string inputMessage,
string model = "gpt-4-turbo-preview",
string serverUrl = "http://localhost:8889/chat",
string cacheKey = null // 新增:用户自定义缓存键
)
{
var actualCacheKey = cacheKey ?? inputMessage;
return AsyncTaskUtil.RunTask(
"RunAgent",
new object[] { actualCacheKey, serverUrl, model }, // 使用cacheKey而不是inputMessage作为任务标识
async () => {
return await RunAgentAsync(inputMessage, model, serverUrl);
});
}
用户可以这样用:
=RunAgent("今天是" & TEXT(A1, "yyyy-mm-dd") & ",请告诉我今天的新闻", , , "daily_news")
这样,即使日期变化,只要cacheKey
是"daily_news",就会使用缓存,除非用户手动清除缓存。
6.4 批量处理优化:一次API调用处理多行
当前的实现是每行一个API调用,如果有1000行,就是1000次调用。可以优化为批量处理:
后端添加批量接口:
class BatchChatInput(BaseModel):
messages: List[str] = Field(description="批量消息列表")
model: str = Field("gpt-4-turbo-preview")
@app.post("/batch_chat")
async def batch_chat(batch_input: BatchChatInput):
tasks = [chat_internal(ChatInput(message=msg, model=batch_input.model))
for msg in batch_input.messages]
results = await asyncio.gather(*tasks)
return results
Excel端添加批量函数:
[ExcelFunction(Name = "RunAgentBatch")]
public static object[,] RunAgentBatch(
[ExcelArgument(AllowReference = true)] object range,
string model = "gpt-4-turbo-preview"
)
{
// 提取range中的所有消息
// 批量发送到后端
// 返回二维数组填充到多个单元格
}
用户可以这样用:
=RunAgentBatch(A2:A1000) // 一次处理999条消息
这种批量处理可以:
-
减少HTTP往返次数(1000次变1次)
-
更好地利用OpenAI的批量API(价格更低)
-
提升整体吞吐量
七、安全性与生产部署
7.1 API密钥管理:不要裸奔
问题: 默认情况下,后端服务监听0.0.0.0:8889
,任何能访问这台机器的人都可以调用你的API,消耗你的OpenAI配额。
解决方案1:添加API密钥认证
from fastapi import Header, HTTPException
API_KEY = os.getenv("API_KEY", "your-secret-key")
async def verify_api_key(x_api_key: str = Header(None)):
if x_api_key != API_KEY:
raise HTTPException(status_code=403, detail="Invalid API Key")
@app.post("/chat", dependencies=[Depends(verify_api_key)])
async def chat(chat_input: ChatInput):
# ...
C#端也要相应修改:
client.DefaultRequestHeaders.Add("X-API-Key", "your-secret-key");
解决方案2:只监听localhost
如果只是本地开发使用:
python langchain_fastapi.py --host 127.0.0.1
这样外部网络无法访问,只有本机的Excel能调用。
7.2 成本控制:别让API费用爆炸
问题: 用户不小心在1000行数据上都用了GPT-4,一觉醒来账单$500。
解决方案:添加配额限制
from collections import defaultdict
from datetime import datetime, timedelta
# 简单的内存配额跟踪(生产环境应该用Redis)
usage_tracker = defaultdict(lambda: {"count": 0, "reset_time": datetime.now()})
@app.post("/chat")
async def chat(chat_input: ChatInput, x_user_id: str = Header("default")):
# 检查配额
user_usage = usage_tracker[x_user_id]
if datetime.now() > user_usage["reset_time"]:
user_usage["count"] = 0
user_usage["reset_time"] = datetime.now() + timedelta(hours=1)
if user_usage["count"] >= 100: # 每小时100次
raise HTTPException(status_code=429, detail="Rate limit exceeded")
user_usage["count"] += 1
# 执行实际逻辑
result = await chat_internal(chat_input)
return result
7.3 输入验证:防止注入攻击
虽然这是内部工具,但也要防止意外或恶意输入:
长度限制:
class ChatInput(BaseModel):
message: str = Field(..., min_length=1, max_length=10000)
内容过滤:
BLOCKED_PATTERNS = [
r"ignore previous instructions",
r"system: you are now",
# 其他Prompt注入模式
]
def validate_input(message: str):
for pattern in BLOCKED_PATTERNS:
if re.search(pattern, message, re.IGNORECASE):
raise ValueError("Potentially malicious input detected")
7.4 日志与监控:知道系统在做什么
添加结构化日志:
import logging
import json
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@app.post("/chat")
async def chat(chat_input: ChatInput):
request_id = str(uuid.uuid4())
logger.info(json.dumps({
"request_id": request_id,
"message_length": len(chat_input.message),
"model": chat_input.model,
"timestamp": datetime.now().isoformat()
}))
start_time = time.time()
result = await chat_internal(chat_input)
duration = time.time() - start_time
logger.info(json.dumps({
"request_id": request_id,
"duration": duration,
"result_length": len(result),
"cached": duration < 0.1 # 快速响应可能来自缓存
}))
return result
这样可以:
-
追踪每个请求的处理时间
-
分析缓存命中率
-
发现性能瓶颈
-
排查错误
7.5 生产部署:Docker化
Dockerfile:
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY langchain_fastapi.py .
COPY .env .
EXPOSE 8889
CMD ["python", "langchain_fastapi.py", "--host", "0.0.0.0"]
docker-compose.yml:
version: '3.8'
services:
excel-agent-api:
build: .
ports:
- "8889:8889"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- TAVILY_API_KEY=${TAVILY_API_KEY}
volumes:
- ./cache:/app/.cache
restart: unless-stopped
部署:
docker-compose up -d
这样可以:
-
环境隔离(不污染主机环境)
-
轻松迁移(打包成镜像,任何地方都能运行)
-
自动重启(崩溃后自动恢复)
-
版本管理(每个版本一个镜像)
八、扩展与定制:让系统更强大
8.1 添加自定义工具:数据库查询
假设你有一个企业数据库,想让Agent能够查询:
步骤1:定义工具函数
from langchain.tools import tool
@tool
def query_company_database(company_name: str) -> str:
"""
Query internal company database to get detailed information.
Useful when you need accurate internal data about a company.
Args:
company_name: The name of the company to query
Returns:
A string containing company details or "Not found" if the company doesn't exist
"""
# 连接数据库(实际应该用连接池)
import sqlite3
conn = sqlite3.connect('companies.db')
cursor = conn.cursor()
cursor.execute("""
SELECT name, employees, revenue, location
FROM companies
WHERE name LIKE ?
""", (f"%{company_name}%",))
result = cursor.fetchone()
conn.close()
if result:
return f"公司名: {result[0]}, 员工数: {result[1]}, 营收: {result[2]}, 地点: {result[3]}"
else:
return "Not found in database"
步骤2:注册工具
async def chat_internal(chat_input: ChatInput):
# 网络搜索工具
web_search_tools = [TavilySearchResults(...)]
# 数据库查询工具
database_tools = [query_company_database]
# 组合所有工具
tools = web_search_tools + database_tools
agent = create_openai_tools_agent(llm=..., tools=tools, prompt=...)
# ...
使用效果:
用户在Excel输入:
=RunAgent("查询微软公司的内部数据")
Agent会:
-
识别这是查内部数据的需求
-
调用
query_company_database
工具 -
返回数据库中的结果
如果数据库没有,Agent会自动fallback到网络搜索。这就是LangChain Agent的强大之处------工具编排完全自动化。
8.2 添加计算工具:让Agent会算数
LLM的数学能力很弱,对于复杂计算(如财务建模),需要专门的工具:
from langchain_experimental.utilities import PythonREPL
@tool
def python_calculator(code: str) -> str:
"""
Execute Python code to perform calculations.
Useful for complex math, data analysis, or numerical simulations.
Args:
code: Python code to execute
Returns:
The output of the code execution
"""
repl = PythonREPL()
try:
result = repl.run(code)
return str(result)
except Exception as e:
return f"Error: {str(e)}"
使用场景:
=RunAgent("如果我投资$10,000,年化收益率8%,30年后复利是多少?")
Agent会生成Python代码:
principal = 10000
rate = 0.08
years = 30
future_value = principal * (1 + rate) ** years
print(future_value)
执行后返回:$100,626.57
这种"LLM + 代码执行"的组合,突破了纯LLM的能力边界。
8.3 添加文件处理工具:读取Excel附件
有时用户希望Agent分析其他Excel文件:
import pandas as pd
@tool
def read_excel_file(file_path: str, sheet_name: str = "Sheet1") -> str:
"""
Read an Excel file and return its content as text.
Args:
file_path: Path to the Excel file
sheet_name: Name of the sheet to read (default: Sheet1)
Returns:
A text representation of the Excel content
"""
try:
df = pd.read_excel(file_path, sheet_name=sheet_name)
# 只返回前10行的摘要,避免context过长
summary = f"文件包含 {len(df)} 行 {len(df.columns)} 列\n"
summary += f"列名: {', '.join(df.columns)}\n"
summary += f"前10行数据:\n{df.head(10).to_string()}"
return summary
except Exception as e:
return f"读取文件失败: {str(e)}"
使用场景:
=RunAgent("分析C:\data\sales.xlsx文件,告诉我销售额最高的前5个产品")
Agent会:
-
调用
read_excel_file
读取文件 -
理解数据结构
-
找出销售额最高的产品
-
返回结果
8.4 多Agent协作:复杂任务的分而治之
对于超复杂任务,单个Agent可能力不从心。可以设计多Agent协作系统:
from langchain.agents import AgentType, initialize_agent
# 研究员Agent:专门做信息搜集
researcher_agent = initialize_agent(
tools=[TavilySearchResults()],
llm=ChatOpenAI(model="gpt-4"),
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True
)
# 分析师Agent:专门做数据分析
analyst_agent = initialize_agent(
tools=[python_calculator, read_excel_file],
llm=ChatOpenAI(model="gpt-4"),
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True
)
# 协调员Agent:分配任务给其他Agent
@tool
def delegate_to_researcher(task: str) -> str:
"""Delegate research tasks to the researcher agent"""
return researcher_agent.run(task)
@tool
def delegate_to_analyst(task: str) -> str:
"""Delegate analysis tasks to the analyst agent"""
return analyst_agent.run(task)
coordinator_agent = initialize_agent(
tools=[delegate_to_researcher, delegate_to_analyst],
llm=ChatOpenAI(model="gpt-4"),
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True
)
使用场景:
=RunAgent("分析特斯拉公司2023年的财务表现,包括股价趋势、营收增长和市场评价")
协调员Agent会:
-
把"搜集特斯拉财务数据"任务分给研究员
-
把"分析股价趋势"任务分给分析师
-
把"搜集市场评价"任务分给研究员
-
综合所有结果,生成最终报告
这种架构可以处理极其复杂的任务,每个Agent专注自己擅长的领域。
8.5 添加记忆系统:让Agent记住上下文
当前实现是无状态的------每次调用都是独立的。但有时我们希望Agent能记住之前的对话:
from langchain.memory import ConversationBufferMemory
# 为每个会话创建独立的记忆(实际应该用Redis等持久化存储)
session_memories = {}
class ChatInput(BaseModel):
message: str
model: str = "gpt-4-turbo-preview"
session_id: str = "default" # 新增:会话ID
async def chat_internal(chat_input: ChatInput):
# 获取或创建该会话的记忆
if chat_input.session_id not in session_memories:
session_memories[chat_input.session_id] = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
memory = session_memories[chat_input.session_id]
# 创建Agent时注入记忆
chain = AgentExecutor(
agent=agent,
tools=tools,
memory=memory, # 注入记忆
verbose=True
)
result = await chain.ainvoke({"input": chat_input.message})
return result["output"]
使用场景:
' A1单元格
=RunAgent("微软公司的CEO是谁?", , , "session1")
' 返回: Satya Nadella
' A2单元格
=RunAgent("他是哪年上任的?", , , "session1")
' 返回: 2014年(Agent记得"他"指的是Satya Nadella)
这种上下文记忆让对话式的数据探索成为可能。
九、性能测试与监控
9.1 压力测试:系统能承受多大负载?
使用locust
进行压力测试:
from locust import HttpUser, task, between
class ExcelAgentUser(HttpUser):
wait_time = between(1, 3)
@task
def query_agent(self):
self.client.post("/chat", json={
"message": "What is the capital of France?",
"model": "gpt-4-turbo-preview"
})
运行测试:
locust -f locustfile.py --host http://localhost:8889
在Web界面设置:
-
用户数:100
-
每秒生成用户数:10
观察指标:
-
响应时间中位数:应该< 5秒(包括LLM调用)
-
95分位响应时间:应该< 15秒
-
错误率:应该< 1%
-
吞吐量:取决于OpenAI的速率限制
优化建议:
如果响应时间过长:
-
检查是否有缓存未命中
-
考虑使用更快的模型(如gpt-3.5-turbo)
-
优化Prompt长度
-
增加并发限制(避免排队)
如果错误率过高:
-
检查是否触发OpenAI速率限制
-
添加重试机制
-
增加超时时间
-
优化错误处理逻辑
9.2 成本监控:不要烧钱
添加Token用量追踪:
from langchain.callbacks import get_openai_callback
async def chat_internal(chat_input: ChatInput):
with get_openai_callback() as cb:
# 执行Agent
result = await chain.ainvoke({"input": chat_input.message})
# 记录用量
logger.info({
"request": chat_input.message[:100],
"total_tokens": cb.total_tokens,
"prompt_tokens": cb.prompt_tokens,
"completion_tokens": cb.completion_tokens,
"total_cost": cb.total_cost
})
return result["output"]
设置预算告警:
DAILY_BUDGET = 50 # 每天$50
daily_cost = 0
async def chat_internal(chat_input: ChatInput):
global daily_cost
with get_openai_callback() as cb:
result = await chain.ainvoke({"input": chat_input.message})
daily_cost += cb.total_cost
if daily_cost > DAILY_BUDGET:
# 发送告警邮件
send_alert(f"Daily budget exceeded: ${daily_cost:.2f}")
# 或者直接停止服务
raise HTTPException(status_code=503, detail="Budget exceeded")
return result["output"]
9.3 实时监控Dashboard
使用Prometheus + Grafana构建监控系统:
安装依赖:
pip install prometheus-client
添加指标导出:
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
# 定义指标
request_count = Counter('agent_requests_total', 'Total number of agent requests')
request_duration = Histogram('agent_request_duration_seconds', 'Request duration')
token_usage = Counter('agent_tokens_used_total', 'Total tokens used')
@app.post("/chat")
async def chat(chat_input: ChatInput):
request_count.inc()
with request_duration.time():
result = await chat_internal(chat_input)
# 假设从callback获取token用量
# token_usage.inc(tokens_used)
return result
@app.get("/metrics")
async def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
Prometheus配置(prometheus.yml):
scrape_configs:
- job_name: 'excel-agent'
scrape_interval: 15s
static_configs:
- targets: ['localhost:8889']
Grafana Dashboard配置:
可视化指标:
-
每分钟请求数(QPS)
-
平均响应时间
-
P95响应时间
-
错误率
-
Token用量趋势
-
预估每日成本
这样可以实时掌握系统健康状况,及时发现异常。
十、未来展望与改进方向
10.1 支持本地LLM:摆脱API依赖
项目Roadmap中提到要支持本地LLM,这是个很有价值的方向:
优势:
-
无API费用(一次性硬件投资)
-
无速率限制(受限于本地算力)
-
数据隐私(敏感数据不出本地)
-
离线可用(不依赖网络)
技术方案:
使用Ollama + LangChain:
from langchain_community.llms import Ollama
# 使用本地Llama 3模型
local_llm = Ollama(model="llama3:70b")
agent = create_openai_tools_agent(
llm=local_llm, # 替换OpenAI模型
tools=tools,
prompt=prompt
)
硬件要求:
-
Llama 3 8B:需要16GB内存,能在普通电脑运行
-
Llama 3 70B:需要80GB显存,需要高端GPU(如A100)
性能对比:
-
GPT-4:质量最高,成本高,有速率限制
-
Llama 3 70B:质量接近GPT-4,免费,受限于硬件
-
Llama 3 8B:质量较低,适合简单任务,能在消费级硬件运行
混合方案:
# 简单任务用本地模型
if is_simple_task(chat_input.message):
llm = Ollama(model="llama3:8b")
else:
llm = ChatOpenAI(model="gpt-4")
这种"本地优先,云端兜底"的策略能平衡成本和质量。
10.2 流式输出:实时看到Agent思考过程
当前实现是等Agent执行完才返回结果,用户看到的是:
输入公式 → #N/A(等待30秒)→ 最终结果
如果支持流式输出,体验会好很多:
输入公式 → "正在搜索..." → "找到3条结果..." → "分析中..." → 最终结果
技术方案:使用Server-Sent Events (SSE)
Python后端:
from fastapi.responses import StreamingResponse
@app.post("/chat_stream")
async def chat_stream(chat_input: ChatInput):
async def generate():
# 使用流式回调
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
callback = StreamingStdOutCallbackHandler()
agent = create_openai_tools_agent(llm=..., tools=..., callbacks=[callback])
async for chunk in agent.astream({"input": chat_input.message}):
yield f"data: {json.dumps(chunk)}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
C#客户端:
使用HttpClient
的ReadAsStreamAsync
处理流式响应,实时更新Excel单元格。这需要修改Excel-DNA的异步处理逻辑,让单元格能够"部分更新"。
10.3 多模态能力:处理图片、图表
Excel不只有文字,还有图片、图表。如果Agent能理解这些:
场景: 用户选中一张图表,然后在旁边的单元格输入:
=RunAgent("分析这张图表的趋势", IMAGE_REF:ChartObject1)
技术方案:使用GPT-4 Vision
from langchain_openai import ChatOpenAI
# 使用支持视觉的模型
vision_llm = ChatOpenAI(model="gpt-4-vision-preview")
# 构造包含图片的消息
from langchain.schema.messages import HumanMessage
message = HumanMessage(
content=[
{"type": "text", "text": "分析这张图表的趋势"},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}}
]
)
result = vision_llm.invoke([message])
C#端需要:
-
捕获图表的引用
-
将图表导出为PNG
-
Base64编码后发送给后端
这样可以实现:
-
自动解读复杂图表
-
提取图片中的数据表格
-
识别手写笔记
-
比较多张图片的异同
10.4 自然语言到Excel公式:反向能力
当前是"Excel公式调用Agent",反过来也很有用:"自然语言生成Excel公式"。
场景: 用户在单元格输入:
计算A列的平均值,忽略空值和错误
然后调用:
=FormulaAgent(C1)
返回:
=AVERAGEIF(A:A, "<>")
技术方案:Few-shot Learning
FORMULA_GENERATION_PROMPT = """
You are an Excel formula expert. Convert natural language requests into Excel formulas.
Examples:
Q: 计算A列的总和
A: =SUM(A:A)
Q: 如果B2大于100,返回"高",否则返回"低"
A: =IF(B2>100,"高","低")
Q: 从C列中提取左边3个字符
A: =LEFT(C:C, 3)
Q: {user_request}
A:
"""
@tool
def generate_formula(description: str) -> str:
"""Generate Excel formula from natural language description"""
llm = ChatOpenAI(model="gpt-4")
prompt = FORMULA_GENERATION_PROMPT.format(user_request=description)
result = llm.invoke(prompt)
return result.content
这能大大降低Excel的学习曲线,让不熟悉函数的用户也能完成复杂计算。
10.5 协作与共享:企业级功能
当前是单用户本地部署,企业使用需要更多功能:
1. 用户管理与权限控制
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
# 验证JWT token,获取用户信息
user = decode_token(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
@app.post("/chat")
async def chat(chat_input: ChatInput, current_user: User = Depends(get_current_user)):
# 检查用户权限
if not current_user.has_permission("use_gpt4") and chat_input.model == "gpt-4":
raise HTTPException(status_code=403, detail="No permission to use GPT-4")
# 记录用户行为
log_usage(current_user.id, chat_input)
result = await chat_internal(chat_input)
return result
2. Agent模板共享
建立一个Agent模板市场:
class AgentTemplate(BaseModel):
name: str
description: str
prompt_template: str
tools: List[str]
author: str
rating: float
@app.get("/templates")
async def list_templates():
# 返回公开的Agent模板
return [
{
"name": "企业调研专家",
"description": "专门用于调研企业信息",
"prompt_template": "...",
"tools": ["tavily_search", "company_database"],
"author": "user123",
"rating": 4.8
},
# ...
]
@app.post("/use_template/{template_id}")
async def use_template(template_id: str, chat_input: ChatInput):
template = get_template(template_id)
# 使用模板配置创建Agent
agent = create_agent_from_template(template)
result = await agent.ainvoke(chat_input.message)
return result
用户可以:
-
浏览热门模板
-
一键应用模板
-
修改模板创建自己的版本
-
分享给团队成员
3. 审计与合规
记录所有Agent调用,便于审计:
from datetime import datetime
import sqlite3
def log_agent_call(user_id: str, input_message: str, output: str, metadata: dict):
conn = sqlite3.connect('audit.db')
cursor = conn.cursor()
cursor.execute("""
INSERT INTO agent_calls (timestamp, user_id, input, output, metadata)
VALUES (?, ?, ?, ?, ?)
""", (datetime.now(), user_id, input_message, output, json.dumps(metadata)))
conn.commit()
conn.close()
这对于:
-
金融行业(需要记录所有决策依据)
-
医疗行业(需要追溯诊断过程)
-
法律行业(需要保留证据链)
等受监管行业至关重要。
10.6 移动端支持:随时随地用Agent
虽然Excel主要在PC上使用,但Excel Mobile(iOS/Android)也很流行。可以开发配套的移动应用:
技术方案:
-
后端不变(FastAPI已经是RESTful API)
-
开发Flutter应用(一次开发,iOS/Android都能用)
-
提供简化的界面(不需要完整的Excel功能)
功能:
-
快速提问(语音输入 → Agent处理 → 语音播报结果)
-
查看历史查询
-
扫描名片 → 自动调研公司信息
-
拍照 → OCR提取文本 → Agent分析
这样可以在开会、出差等场景下,也能使用Agent能力。
十一、最佳实践与设计模式
11.1 Prompt Engineering:如何写出好提示词
好的提示词能让Agent性能翻倍:
❌ 坏例子:
=RunAgent("告诉我这个公司的信息: " & A2)
问题:
-
太模糊("信息"是什么?)
-
没有格式要求(可能返回长篇大论)
-
没有错误处理(如果公司不存在怎么办?)
✅ 好例子:
=RunAgent("调研公司:" & A2 & "。请返回:1)员工数(仅数字)2)总部地点(城市名)3)主营业务(20字以内)。如果找不到信息,返回'未找到'。格式:数字|地点|业务")
优点:
-
明确任务范围
-
指定输出格式(便于后续解析)
-
设置字数限制(节省token)
-
定义错误情况的处理
通用模板:
角色定位:你是[专业角色]
任务描述:请[具体动作][具体对象]
输出要求:
- 格式:[具体格式]
- 长度:[字数限制]
- 风格:[正式/简洁/详细]
特殊情况:如果[条件],则[行动]
输入数据:{cell_reference}
11.2 错误处理:优雅地失败
分层错误处理策略:
Layer 1: 网络层
try {
var response = await client.PostAsync(serverUrl, content);
if (!response.IsSuccessStatusCode) {
return $"服务器错误 ({response.StatusCode})";
}
} catch (HttpRequestException ex) {
return "网络连接失败,请检查后端服务是否启动";
} catch (TaskCanceledException ex) {
return "请求超时,任务可能过于复杂";
}
Layer 2: 业务层
try:
result = await chain.ainvoke({"input": message})
except Exception as e:
if "rate_limit" in str(e).lower():
return "API速率限制,请稍后重试"
elif "context_length" in str(e).lower():
return "输入文本过长,请精简后重试"
else:
logger.error(f"Agent execution failed: {e}")
return f"处理失败: {str(e)[:100]}"
Layer 3: Agent层
chain = AgentExecutor(
agent=agent,
tools=tools,
handle_parsing_errors=True, # 自动处理解析错误
max_execution_time=300, # 5分钟超时
early_stopping_method="generate" # 超时时生成部分结果
)
这种分层设计确保:
-
用户总能得到有用的反馈(而不是神秘的错误码)
-
开发者能定位问题(日志记录详细信息)
-
系统能自我恢复(自动重试、降级)
11.3 性能优化:从秒级到毫秒级
优化1:提前编译Prompt模板
from langchain.prompts import PromptTemplate
# ❌ 每次都解析模板(慢)
def bad_approach():
prompt = PromptTemplate.from_template("...")
chain = prompt | llm
return chain.invoke(...)
# ✅ 启动时编译一次(快)
COMPILED_PROMPT = PromptTemplate.from_template("...")
COMPILED_CHAIN = COMPILED_PROMPT | llm
def good_approach():
return COMPILED_CHAIN.invoke(...)
优化2:连接池复用
// ❌ 每次创建新的HttpClient(慢,且可能耗尽端口)
public static async Task<string> Bad()
{
HttpClient client = new HttpClient(); // 每次都创建
var response = await client.PostAsync(...);
return await response.Content.ReadAsStringAsync();
}
// ✅ 复用HttpClient(快,且资源友好)
private static readonly HttpClient SharedClient = new HttpClient()
{
Timeout = TimeSpan.FromMinutes(10)
};
public static async Task<string> Good()
{
var response = await SharedClient.PostAsync(...);
return await response.Content.ReadAsStringAsync();
}
优化3:批量嵌入缓存
如果使用向量搜索(如RAG场景),批量计算嵌入:
# ❌ 逐个计算(慢)
embeddings = [embedding_model.embed(text) for text in texts]
# ✅ 批量计算(快10倍)
embeddings = embedding_model.embed_batch(texts)
11.4 测试策略:如何测试AI系统
AI系统的测试和传统软件不同,因为输出不是确定的:
1. 单元测试:测试确定性部分
def test_json_escaping():
input_text = 'He said: "Hello\nWorld"'
expected = 'He said: \\"Hello\\nWorld\\"'
assert escape_for_json(input_text) == expected
def test_cache_hit():
input1 = ChatInput(message="test", model="gpt-4")
result1 = await chat_internal(input1)
start_time = time.time()
result2 = await chat_internal(input1) # 第二次应该从缓存读取
duration = time.time() - start_time
assert result1 == result2
assert duration < 0.1 # 缓存读取应该很快
2. 集成测试:测试端到端流程
@pytest.mark.asyncio
async def test_simple_query():
input_data = {"message": "What is 2+2?", "model": "gpt-3.5-turbo"}
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post("/chat", json=input_data)
assert response.status_code == 200
result = response.json()
assert "4" in result # 答案应该包含"4"
3. 质量测试:测试AI输出质量
# 准备测试集
test_cases = [
{
"input": "微软公司的CEO是谁?",
"expected_keywords": ["Satya Nadella", "纳德拉"],
"not_expected": ["比尔盖茨", "已卸任"] # 常见错误
},
# ...
]
def test_agent_quality():
for case in test_cases:
result = agent.invoke(case["input"])
# 检查是否包含期望的关键词(至少一个)
assert any(kw in result for kw in case["expected_keywords"]), \
f"Expected keywords not found in: {result}"
# 检查是否包含不应该出现的内容
assert not any(kw in result for kw in case["not_expected"]), \
f"Unexpected keywords found in: {result}"
4. 性能基准测试
def test_performance_benchmark():
test_queries = [
"简单的数学问题: 3+5=?",
"中等复杂度: 分析苹果公司的业务模式",
"高复杂度: 比较特斯拉和比亚迪的电动车技术优劣"
]
for query in test_queries:
start = time.time()
result = agent.invoke(query)
duration = time.time() - start
# 记录基准时间
benchmark_results[query] = duration
# 确保不退步(允许10%的波动)
if query in historical_benchmarks:
assert duration < historical_benchmarks[query] * 1.1, \
f"Performance regression detected: {duration}s vs {historical_benchmarks[query]}s"
十二、总结与思考
12.1 技术亮点回顾
ExcelAgentTemplate虽然代码量不大(Python后端不到150行,C#前端不到200行),但设计思想非常先进:
-
架构解耦:前后端分离,各自发挥所长
-
异步优先:全链路异步,用户体验流畅
-
缓存至上:多层缓存,性能和成本兼顾
-
可扩展性:基于LangChain,轻松添加新能力
-
用户友好:原生Excel体验,零学习成本
这种"小而美"的设计,比那些动辄上万行代码的框架更值得学习。
12.2 适用场景的边界
ExcelAgentTemplate不是银弹,它有明确的适用边界:
✅ 适合的场景:
-
数据调研与收集
-
内容生成与翻译
-
数据清洗与标准化
-
信息提取与分类
-
简单的决策辅助
❌ 不适合的场景:
-
实时性要求极高(< 1秒响应)
-
需要100%准确率(如财务计算)
-
大规模数据处理(百万行级别)
-
复杂的业务逻辑(应该写专门的程序)
了解边界,才能用得其所。
12.3 对未来的启示
ExcelAgentTemplate代表了一个趋势:AI能力的民主化。
传统的AI应用开发需要:
-
懂机器学习(训练模型)
-
懂后端开发(部署服务)
-
懂前端开发(构建界面)
而现在,有了LLM和像ExcelAgentTemplate这样的工具,普通人也能:
-
在Excel里调用世界级AI
-
用自然语言描述需求
-
几分钟构建自动化流程
这种"低代码/无代码"的AI应用,会让更多人受益于AI技术。
12.4 给开发者的建议
如果你想基于ExcelAgentTemplate开发自己的应用:
1. 从简单开始
-
先让一个基础场景跑通
-
不要一开始就追求完美
-
迭代优化,逐步增加功能
2. 重视Prompt Engineering
-
好的Prompt能让普通模型发挥出强大能力
-
建立Prompt模板库
-
不断测试和优化
3. 监控和优化
-
记录每次调用的成本和耗时
-
定期分析使用模式
-
找到优化空间
4. 安全和合规
-
敏感数据不要发给外部API
-
添加访问控制和审计日志
-
定期备份重要数据
5. 用户教育
-
告诉用户AI的能力和局限
-
提供最佳实践指南
-
收集反馈,持续改进
结语:Excel的第二次生命
35年前,Excel彻底改变了商业世界------让复杂的表格计算变得人人可用。
今天,ExcelAgentTemplate带来了Excel的第二次革命------让强大的AI能力变得触手可及。
从=SUM(A1:A10)
到=RunAgent("帮我分析这份数据")
,这不仅仅是技术的进步,更是人机交互范式的革命。我们不再需要记住晦涩的函数语法,只需要用人类的语言描述需求。
这个项目的代码不多,但思想深刻。它告诉我们:
-
好的技术应该是invisible的(用户感觉不到技术的存在)
-
好的架构应该是simple的(核心逻辑清晰明了)
-
好的产品应该是empowering的(让用户能做到以前做不到的事)
如果你是Excel的重度用户,这个工具能让你的效率提升10倍。 如果你是开发者,这个项目能给你很多架构设计的启发。 如果你是AI从业者,这个案例展示了如何把AI真正落地到实际业务。
技术的意义,不在于炫技,而在于让人们的生活更美好。ExcelAgentTemplate做到了。
其他文档: