functools 的 lru cache、partial、reduce 在 Agent 中如何应用,各自有什么限制?
面试中如果问到 functools 里这几个工具在 Agent 中的应用,面试官通常不是考 API 怎么用,而是想看你如何用函数式思维降低系统复杂度。我会把这三个分别对应到 Agent 的三个典型痛点:重复计算、参数膨胀、流式聚合。
💾 lru_cache —— 给 Agent 的记忆加个“缓存条”¶
本质:记住函数调用的结果,相同输入直接返回缓存,不用再算。
在 Agent 中的典型用法
-
缓存工具调用结果:比如天气查询、汇率转换,短时间内相同参数的结果是稳定的。Agent 反复调用时,直接命中缓存,既快又省 API 配额。
-
缓存 LLM 响应:如果某些 prompt 是固定的模板+参数,可对组装后的请求做缓存。但要注意,LLM 本身有随机性,通常只缓存
temperature=0的确定性调用。
from functools import lru_cache
@lru_cache(maxsize=128)
def get_weather(city: str, date: str):
return weather_api.fetch(city, date)
⚠️ 限制与坑
| 限制 | 说明 |
|---|---|
| 🔐 参数必须可哈希 | 像 list、dict 不能直接做参数,得转成 tuple 或冻结的 dataclass。Agent 的 message 列表经常要 str() 或序列化后做 key。 |
| 🧠 内存无上限的风险 | 必须设 maxsize,不然 Agent 跑久了会撑爆内存。我习惯按任务量设个几百,再配合定时清理。 |
| 🌪️ 不适合异步函数 | lru_cache 是同步的,在 async def 上直接加,底层会出问题。需要 async_lru 这个第三方库,或者自己用 asyncio.Lock + dict 实现。 |
| 🔄 副作用陷阱 | 缓存了有副作用的函数(如写库、发通知),会导致操作被“吞掉”。Agent 里只缓存纯查询类工具。 |
🔧 partial —— 冻结高频参数,让 Agent 的指令更简洁¶
本质:预先填好函数的一部分参数,生成一个新函数,调用时只需补上剩余参数。
在 Agent 中的典型用法
-
预设工具默认值:比如公司内部的搜索工具总是搜内网、用特定语言,每次调都传一样参数很繁琐。
partial把底细藏起来,给 Agent 暴露一个简洁接口。 -
统一回调接口:Agent 各模块之间需要回调,但不同模块的签名不同。可以用
partial把上下文(如 session_id)提前绑死,让接口统一为callback(result)。 -
为 LLM 函数调用瘦身:OpenAI function calling 定义的参数有时很灵活,通过
partial可以生成“特化版”函数,减少 Agent 决策时选错参数的概率。
from functools import partial
# 原始工具:search(query, source, lang)
internal_search = partial(search, source="internal_wiki", lang="zh")
# 现在 Agent 只需调用 internal_search(query)
⚠️ 限制与坑
-
过早求值:
partial绑定的参数在定义时就求值了,如果绑了一个可变对象(比如一个列表),后来它变了,partial里还是旧的。解决方案是绑不可变数据或对象引用。 -
可读性下降:连续几个
partial嵌套后,debug 时看到函数叫partial(func, ...)会丧失签名信息。可以在定义后手动修改name或使用functools.wraps。 -
参数覆盖风险:调用时如果又传了已固定的参数,会抛出
TypeError: got multiple values,在动态 Agent 拼装参数时容易踩。
➰ reduce —— 把一连串工具输出揉成一个结果¶
本质:对一个序列反复应用一个二元函数,最终聚合成一个值。
在 Agent 中的典型用法
-
多路搜索合并:Agent 同时调了 3 个搜索引擎,返回了 3 段文本。用
reduce+operator.concat或自定义合并函数,把结果串成一段完整的上下文喂给 LLM。 -
置信度累积:多个分析模块各自给出一个可信度分数(0~1),用
reduce相乘或取平均,得到整体置信度,决定是否需要人工介入。 -
管道式数据处理:把清洗、过滤、排序等操作串成一条链,
reduce搭配partial可以写出非常紧凑的数据流。
from functools import reduce
import operator
results = [
"北京今日晴朗,25度。",
"北京今天多云,26度。",
"北京当前温度24度。"
]
merged = reduce(operator.add, results)
# 或者更复杂的聚合:
def merge_two(a, b):
return f"{a}\n---\n{b}"
context = reduce(merge_two, results)
⚠️ 限制与坑
-
可读性争议:Guido 一度想把它从内置移除,因为显式
for循环往往更易懂。Agent 流程中如果逻辑复杂,别炫技,for循环加注释更实在。 -
空序列会炸:
reduce对空序列不传初始值会TypeError。永远传第三个参数initial,比如reduce(merge_two, results, "")。 -
顺序依赖:
reduce是严格从左到右的,如果合并操作不满足结合律,结果会依赖顺序。在多工具并行结果合并时,得保证顺序对你是有意义的,或者先排序。
🧩 三者怎么打出组合拳?¶
| 工具 | 解决痛点 | 一句话口诀 |
|---|---|---|
| lru_cache | 重复计算 | 算过的别再算 |
| partial | 参数爆炸 | 重复的提前填 |
| reduce | 结果分散 | 零碎的揉成团 |
实际在 Agent 里,经常是 partial 生成特化工具 → lru_cache 缓存该工具结果 → 最后 reduce 将多个缓存结果汇总。函数式三件套,让代码像搭乐高一样拼接,而不是每次都手写状态管理。
写 Agent 最怕的不是逻辑复杂,而是同样的信息被反复计算、同样的参数被反复传递、同样的结果被割裂存储。
functools里的这几个老伙计,帮你把这三件事压到最低——记住,它们是 Python 给开发者的一场“记忆移植手术”,让你把精力放在流程上,而不是细节上。