UseFunctionInvocation 输出乱码和工具调用文本,如何排查?
结论
仅凭这段输出,无法确定问题是由 phi4-mini、Ollama、客户端适配层还是字符编码造成的。应先确认 GetSymbolFromOpenFigi 是否实际执行,以及模型返回的是结构化工具调用,还是带有工具调用标记的普通文本。
UseFunctionInvocation() 负责处理客户端能够识别的函数调用并继续对话,但它不能保证模型一定生成有效调用,也不会自动修复乱码或不符合协议的文本。
输出中的线索说明了什么
问题中的请求是查询 BBG000BBD070 对应的 symbol,但输出出现了:
<|tool_call|>
[Pause for effect as the assistant would perform the function internally]
后一句描述的是“准备执行函数”的过程,不能证明函数已经执行。如果 <|tool_call|> 直接出现在普通文本中,也不能仅凭这一标记认定客户端收到了有效工具调用。
这类现象需要检查模型输出格式、模型模板和客户端对工具调用的解析是否匹配。现有材料还不足以确定问题出在哪一项。输出中的 AT&T Inc. 和 T 也不能视为数据库查询结果,应核对工具实际返回的数据。
连续的 ? 需要单独排查:它们可能已经存在于模型响应中,也可能是输出或复制时经过编码转换产生的。
先确认工具是否执行
在 GetSymbolFromOpenFigi 的入口设置断点,或添加一条写入标准错误流的诊断日志:
Console.Error.WriteLine(
$"GetSymbolFromOpenFigi called: {bloombergId}");
同时观察数据库查询是否完成、返回了什么,以及返回后是否有后续模型请求。
根据观察结果继续定位:
- 函数没有进入:检查请求是否携带工具定义,以及响应是否包含客户端可识别的结构化调用。
- 函数进入但查询失败:先处理连接、查询或权限问题。
- 函数正常返回,但最终回答仍异常:检查工具返回的数据、后续请求,以及模型生成最终回答的过程。
原代码在异常处理中使用 Console.WriteLine(e),而模型文本也写入同一个输出流。排查时应将异常日志写到标准错误流,以免应用日志与模型回复混在一起。
用单个工具缩小问题范围
当前问题只需要查询标识符,用不到价格和日期工具。可以保留原来的 client,暂时将调用部分简化为:
var diagnosticOptions = new ChatOptions
{
Tools =
[
AIFunctionFactory.Create(GetSymbolFromOpenFigi)
]
};
var diagnosticConversation = new List<ChatMessage>
{
new(ChatRole.System,
"Use GetSymbolFromOpenFigi to look up the requested identifier. " +
"Answer using the tool result. If no symbol is available, say so."),
new(ChatRole.User,
"What is Bloomberg Id BBG000BBD070 symbol?")
};
await foreach (var response in client.GetStreamingResponseAsync(
diagnosticConversation, diagnosticOptions))
{
Console.Write(response.Text);
}
这样可以减少干扰,但修改提示词并不一定能修复协议问题。如果仍然只有描述工具调用的文本,而且函数没有执行,应继续检查请求和响应的结构。
检查工具调用经过的各个环节
除了 response.Text,还需要观察工具调用的完整过程:
- 发给 Ollama 的请求中,是否存在工具名称、描述和参数定义。
- Ollama 的响应中,工具调用是否位于专门的结构化字段,还是混在正文里。
- 客户端是否将该响应解析为函数调用内容。
- 工具执行后,结果是否作为工具消息发送给模型。
- 最终模型响应中是否仍包含异常标记。
如果服务端已经返回结构化调用,但客户端没有识别,应检查适配层和相关包的兼容性。如果服务端返回的是普通文本形式的调用标记,则需要检查模型实际使用的 tag、模板和工具调用配置。
问题没有给出 Microsoft.Extensions.AI、提供 OllamaChatClient 的包和 Ollama 的版本,也没有给出完整模型信息,因此不能断言某个版本存在缺陷,也不能认定所有 phi4-mini 配置都不支持工具调用。进一步求助时,应补充这些信息,以及脱敏后的请求和响应。
还可以使用相同输入对比流式和非流式调用。如果只有流式调用异常,可重点检查流式调用片段的解析和组装,并通过响应记录确认。
分开处理编码和数据库问题
判断问号在哪一层出现
可以先验证运行环境能否正常显示中文:
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine("中文显示测试");
然后比较服务端原始响应、客户端收到的文本和终端显示内容。如果前两者已经是字面量 ?,修改终端编码无法恢复原字符;如果原始文本正常而终端显示异常,再检查终端、重定向文件或日志采集环节的编码。
核对连接地址和工具返回值
示例中有多个主机名写成了 locahost。如果实际目标是本机,且没有配置这个主机名,应改为 localhost。如果它只是示例中的替换值,则无需据此判断故障原因。连接失败通常会产生连接异常,不能直接解释工具调用标记为何泄漏。
MongoDB 查询还应明确区分“没有记录”和“查询失败”。查询后的处理可以写成:
var document = await collection.Find(filter).FirstOrDefaultAsync();
if (document is null)
{
return "{\"found\":false}";
}
return document.ToJson();
原代码捕获所有异常后返回 "Not found",会让模型无法区分连接失败与确实没有记录。异常应单独记录,工具返回值也应表达真实状态。
确认查询正常后,尽量只返回回答需要的字段,并清楚标明哪个字段是 symbol。字段名称和含义应以实际数据库结构为准,不能从模型生成的答案反推。
另外,GetLastDailyPrice 将模型提供的 bloombergId 直接拼接进 SQL,应改为参数化查询。虽然这不能直接说明当前乱码现象的原因,但需要在正式使用前处理。
备注:内容仅供参考。
