共计 3128 个字符,预计需要花费 8 分钟才能阅读完成。
从智能客服看函数调用挑战
最近在帮某电商平台改造智能客服系统时,遇到这样的场景:当用户输入 ” 我想退货上周买的羽绒服 ” 时,后台需要依次执行 订单查询 (用户 ID)→退货政策检查(订单号)→生成退货码 三个函数。传统反射调用平均耗时 37ms,在高并发时直接成为瓶颈。更棘手的是,大模型返回的参数可能是 "上周" 这样的模糊值,需要先转换成 DateTime 才能传给业务函数。

三种技术路线性能对决
先用 BenchmarkDotNet 对比常见方案(测试机器:i7-12700H, 32GB DDR5):
[Benchmark]
public void ReflectionInvoke()
{typeof(OrderService).GetMethod("GetLastOrder")
.Invoke(service, new object[] {userId});
}
[Benchmark]
public void DynamicMethodInvoke()
{// 动态 IL 生成代码省略...}
[Benchmark]
public void ExpressionTreeInvoke()
{var compiled = _orderCache[typeof(OrderService)];
compiled.DynamicInvoke(service, userId);
}
测试结果:
| 方案 | 调用耗时 | GC 压力 |
|---|---|---|
| 反射调用 | 37.2ns | 3 alloc |
| 动态方法 | 12.8ns | 0 alloc |
| 表达式树(缓存) | 2.1ns | 0 alloc |
表达式树方案深度实现
1. 类型安全映射构建
核心是通过泛型约束建立强类型字典:
private static ConcurrentDictionary<Type, Delegate> _funcCache = new();
public void RegisterFunc<T>(string funcName, Expression<Func<T, object>> expr)
{var method = ((MethodCallExpression)expr.Body).Method;
var param = Expression.Parameter(typeof(object[]));
// 构建参数转换表达式
var callParams = method.GetParameters().Select((p, i) =>
Expression.Convert(Expression.ArrayIndex(param, Expression.Constant(i)),
p.ParameterType));
_funcCache[typeof(T)] = Expression.Lambda(Expression.Call(instance, method, callParams),
param).Compile();}
2. 冷启动优化技巧
使用 Lazy<T> 延迟编译,首次调用时间从 200ms 降至 5ms:
private Lazy<Func<object[], object>> _lazyFunc;
public FunctionWrapper(MethodInfo method)
{_lazyFunc = new Lazy<Func<object[], object>>(() =>
{// 表达式树编译逻辑}, LazyThreadSafetyMode.ExecutionAndPublication);
}
3. 异步方法处理
需要特殊处理 Task<T> 的嵌套情况:
if (method.ReturnType.IsGenericType
&& method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
{var taskResultType = method.ReturnType.GetGenericArguments()[0];
var castResult = Expression.Convert(result, typeof(object));
if (taskResultType.IsGenericType
&& taskResultType.GetGenericTypeDefinition() == typeof(Task<>))
{
return Expression.Call(typeof(TaskExtensions), "Unwrap",
new[] { taskResultType},
castResult);
}
}
生产级代码示例
完整路由实现包含这些关键点:
// 线程安全路由表
public class FunctionRouter
{private ConcurrentDictionary<string, FunctionWrapper> _routes = new();
public async Task<object> ExecuteAsync(string funcName, object[] args)
{if (!_routes.TryGetValue(funcName, out var wrapper))
throw new KeyNotFoundException($"Function {funcName} not registered");
try
{
// 参数自动转换逻辑
var convertedArgs = wrapper.ConvertArgs(args);
return await wrapper.InvokeAsync(convertedArgs);
}
catch (Exception ex)
{
// Polly 熔断会捕获这里抛出的异常
throw new FunctionInvocationException(funcName, ex);
}
}
}
// 用 Source Generator 生成代理类
[FunctionProxy]
public partial class OrderServiceProxy
{[Function("get_last_order")]
public Task<Order> GetLastOrderAsync(long userId) {...}
}
生产环境生存指南
1. 内存泄漏排查
动态程序集加载务必注意:
// 错误示范:每次调用都 new AssemblyBuilder
var assembly = AssemblyBuilder.DefineDynamicAssembly(...);
// 正确做法:全局缓存
private static readonly ModuleBuilder _moduleBuilder =
AssemblyBuilder.DefineDynamicAssembly(...).DefineDynamicModule(...);
2. Polly 熔断策略
针对大模型特有的超长响应:
Policy.Handle<FunctionInvocationException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromMinutes(1),
onBreak: (ex, delay) => {/* 报警逻辑 */},
onReset: () => { /* 恢复通知 */});
3. 分布式版本兼容
通过函数指纹实现多版本共存:
// 函数名 + 参数类型 hash 作为 key
v1/get_order_by_id(long)
v2/get_order_by_id(string)
未完的思考:AOT 兼容难题
当项目需要 Native AOT 编译时,动态代码生成会变成拦路虎。可能的解决方向:
- 预生成所有可能的函数组合(牺牲灵活性)
- 改用基于接口的静态代理(需设计时注册)
- 等待.NET 的运行时动态代码生成 AOT 支持
你更倾向哪种方案?欢迎在评论区分享实战经验。
正文完
