C# 使用扣子API 实现附带文件上传的AI对话功能
需求
精力能力有限,使用 deepseek 等模型没有找到附带上传文件的 API 来实现 AI 对话,公司咨询项目组最近都在使用豆包 AI 对话,认为解答的比其它智能体都比较“精准”。为实现高效率办公业务需求,决定注册火山引擎平台来模拟实现豆包AI的调用,也没有找到文件上传的API功能。
火山引擎注册地址如下:https://console.volcengine.com/auth/login
于是与豆包对话询问是否能够提供文件上传功能的API,同样的提示词,输出了不同的,许多让人迷茫的回答:第一个回复说豆包平台API暂不支持文件上传功能,建议开发者自行解析上传文档内容并组合成提示语进行会话;第二个回复给了点儿希望,说是访问豆包开放者平台,能供文件上传功能,于是点击提供的链接,发现已无效。
再次回到火山引擎,发现在火山方舟 -> 我的应用 里有一个 coze (扣子) 平台:

跳转到 coze 平台,根据以往“经验”,先没有着急创建应用和体验,直接点击左侧菜单栏的 文档中心 -> API 和 SDK -> 文件 -> 上传文件,终于找到了实现的支持。

文件上传实现
调用扣子 API 之前需要在 API管理 ->授权-> 个人访问令牌,创建访问令牌:

然后就可以正常调用扣子提供的 API 功能了,我们创建一个 Uploader 类,基本说明如下表:
| 序号 | 成员名称 | 成员类型 | 类型 | 说明 |
|---|---|---|---|---|
| 1 | PostUrl | 属性 | string | 访问的 COZE API 地址 |
| 2 | ApiKey | 属性 | string | 在COZE平台申请的访问令牌 |
| 3 | ErrorMessage | 属性 | string | 错误返回信息 |
| 4 | ResultJson | 属性 | string | 正常调用返回的JOSN |
| 5 | PostData | 属性 | List<PostFileItem> | PostFileItem 类表示一个上传列表项,可能包含键值或文件。项的类型枚举为: enum PostFileItemType |
| 6 | AddKey(string key, string value) | 方法 | void | 添加用于上传的一个POST键值 |
| 7 | AddFile(string keyname, string srcFileName, string contentType = "text/plain") | 方法 | void | 添加用于上传的一个文件 |
| 8 | coze_upload() | 方法 | string | 调用 COZE 上传文件API |
完整示例代码如下:
public class Uploader{public CosysJaneCommonAPI.FileEx fe = new CosysJaneCommonAPI.FileEx();public string PostUrl { get; set; }public string ApiKey { get; set; }public string ErrorMessage = "";public string ResultJson = "";public List<PostFileItem> PostData { get; set; }public Uploader(){this.PostData = new List<PostFileItem>();}public void AddKey(string key, string value){this.PostData.Add(new PostFileItem { Name = key, Value = value });}public void AddFile(string keyname, string srcFileName, string contentType = "text/plain"){string[] srcName = Path.GetFileName(srcFileName).Split('.');string exName = "";if (srcName.Length > 1){exName = "." + srcName[srcName.Length - 1];}ReadyFile(keyname, GetBinaryData(srcFileName), exName, contentType);}void ReadyFile(string name, byte[] fileBytes, string fileExName = "", string contentType = "text/plain"){this.PostData.Add(new PostFileItem{Type = PostFileItemType.File,Name = name,FileBytes = fileBytes,FileName = fileExName,ContentType = contentType});}public string coze_upload(){this.PostUrl = "https://api.coze.cn/v1/files/upload";var boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x");var request = (HttpWebRequest)WebRequest.Create(this.PostUrl);request.ContentType = "multipart/form-data; boundary=" + boundary;request.Method = "POST";request.KeepAlive = true;request.Headers.Add("Authorization:Bearer " + ApiKey + "");Stream memStream = new System.IO.MemoryStream();var boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");var endBoundaryBytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--");var formdataTemplate = "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"{0}\";\r\n\r\n{1}";var formFields = this.PostData.Where(m => m.Type == PostFileItemType.Text).ToList();foreach (var d in formFields){var textBytes = System.Text.Encoding.UTF8.GetBytes(string.Format(formdataTemplate, d.Name, d.Value));memStream.Write(textBytes, 0, textBytes.Length);}const string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";var files = this.PostData.Where(m => m.Type == PostFileItemType.File).ToList();foreach (var fe in files){memStream.Write(boundarybytes, 0, boundarybytes.Length);var header = string.Format(headerTemplate, fe.Name, fe.FileName ?? "System.Byte[]", fe.ContentType ?? "text/plain");var headerbytes = System.Text.Encoding.UTF8.GetBytes(header);memStream.Write(headerbytes, 0, headerbytes.Length);memStream.Write(fe.FileBytes, 0, fe.FileBytes.Length);}memStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);request.ContentLength = memStream.Length;HttpWebResponse response;try{using (var requestStream = request.GetRequestStream()){memStream.Position = 0;var tempBuffer = new byte[memStream.Length];memStream.Read(tempBuffer, 0, tempBuffer.Length);memStream.Close();requestStream.Write(tempBuffer, 0, tempBuffer.Length);}response = (HttpWebResponse)request.GetResponse();}catch (WebException webException){response = (HttpWebResponse)webException.Response;}if (response == null){ErrorMessage = "HttpWebResponse is null";}var responseStream = response.GetResponseStream();if (responseStream == null){ErrorMessage = "ResponseStream is null";}using (var streamReader = new StreamReader(responseStream)){ResultJson = streamReader.ReadToEnd();return ResultJson;}}byte[] GetBinaryData(string filename){if(!File.Exists(filename)){return null;}try{FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read);byte[] imageData = new Byte[fs.Length];fs.Read( imageData, 0,Convert.ToInt32(fs.Length));fs.Close();return imageData;}catch(Exception){return null;}finally{}} }public class PostFileItem{public PostFileItem(){this.Type = PostFileItemType.Text;}public PostFileItemType Type { get; set; }public string Value { get; set; }public byte[] FileBytes { get; set; }public string Name { get; set; }public string FileName { get; set; }public string ContentType { get; set; }}public enum PostFileItemType{Text = 0,File = 1}
成功调用会返回如下JSON:
{"code": 0,"data": {"bytes": 152236,"created_at": 1715847583,"file_name": "x.docx","id": "73694"},"msg": ""
}
其中的 id 就是上传成功后存储在COZE服务器的文件id,按文档说明是有有效期的(3个月),如果做为临时使用文件据说是24个小时,总之需要按照我们实际的业务进行考量。上传多个文件则按照上述步骤以此类推,然后就可以进行对话功能的实现了。
AI对话实现
实现AI对话前,需要在 COZE 开发平台创建项目(智能体),如下:

然后编辑智能体,为其添加链接解析插件,如下图:

另外,我们需要编辑个人访问令牌的 API 列表授权功能,如下图:

coze_chat 方法提供了 AI 对话功能,基本说明如下表:
然后就可以正常调用扣子提供的 API 功能了,我们创建一个 Uploader 类,基本说明如下表:
| 序号 | 参数名称 | 参数类型 | 说明 |
|---|---|---|---|
| 1 | user_id | string | 对话的自定义用户ID 字符串,比如123456 |
| 2 | say | string | 提问关键词 |
| 3 | BotID | string | 申请的智能体ID,要通过编辑智能体项目,通过浏览器地址的最后部分查看,比如 https://www.coze.cn/space/75/bot/664277 那么 664277即为申请的 BotID |
| 4 | file_id_list | string | 上传成功后获取的 file_id 列表(文件类型),多个id 以逗号分隔 |
| 5 | img_id_list | string | 上传成功后获取的 file_id 列表(图片类型),多个id 以逗号分隔 |
完整示例代码如下:
string ErrorMessage = "";
string ResultJson = "";
public void coze_chat(string user_id,string say, string BotID = "",string file_id_list="",string img_id_list=""){say = say.Replace("\r", "\\r").Replace("\n","\\n");ApiUrl = "https://api.coze.cn/v3/chat";string content_type="text";string[] file_list = file_id_list.Split(',');if (file_id_list != "" || img_id_list != ""){content_type = "object_string";}WebService ws = new WebService();string[] headers = new string[3];headers[0] = "Content-Type:application/json";headers[1] = "Accept:application/json";headers[2] = "Authorization:Bearer " + ApiKey + "";string jsoncontent = "{";jsoncontent+= "\"bot_id\":\""+BotID+"\",";jsoncontent += "\"user_id\":\"" + user_id + "\",";jsoncontent += "\"stream\":false,";jsoncontent += "\"auto_save_history\":true,";jsoncontent += "\"additional_messages\":[{";jsoncontent += "\"role\":\"user\",";jsoncontent += "\"content\":\"[";if (content_type == "object_string"){jsoncontent += "{\\\"type\\\":\\\"text\\\",";jsoncontent += "\\\"text\\\":\\\"" + say + "\\\"},";if (file_id_list != ""){for (int i = 0; i < file_list.GetLength(0); i++){jsoncontent += "{\\\"type\\\":\\\"file\\\",";jsoncontent += "\\\"file_id\\\":\\\"" + file_list[i] + "\\\"},";}}jsoncontent = jsoncontent.Substring(0, jsoncontent.Length - 1);jsoncontent += "]\",";jsoncontent += "\"content_type\":\"" + content_type + "\"";jsoncontent += "}]}";}string postData = jsoncontent;ErrorMessage = "";ResultJson = "";string rs = ws.GetResponseResult(ApiUrl, Encoding.UTF8, "POST", postData, headers);ErrorMessage = ws.ErrorMessage;ResultJson = rs;}
其中 WebService 类示例代码如下:
public sealed class WebService{#region Internal Memberspublic string ErrorMessage = "";#endregion/// <summary>/// 构造函数,提供初始化数据的功能,打开Ftp站点/// </summary>public string GetResponseResult(string url, System.Text.Encoding encoding, string method, string postData){return GetResponseResult(url,encoding,method,postData,null);}private static bool validSecurity(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors){return true;}public string GetResponseResult(string url, System.Text.Encoding encoding, string method, string postData,string[] headers,string ContentType= "application/x-www-form-urlencoded",bool secValid=true){method = method.ToUpper();if (secValid == false){ServicePointManager.ServerCertificateValidationCallback = validSecurity;}System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls | System.Net.SecurityProtocolType.Tls11 | System.Net.SecurityProtocolType.Tls12;if (method == "GET"){try{WebRequest request2 = WebRequest.Create(@url);request2.Method = method;if (headers != null){for (int i = 0; i < headers.GetLength(0); i++){if (headers[i].Split(':').Length < 2){continue;}if (headers[i].Split(':').Length > 1){if (headers[i].Split(':')[0] == "Content-Type"){request2.ContentType = headers[i].Split(':')[1];continue;}}request2.Headers.Add(headers[i]);}}WebResponse response2 = request2.GetResponse();try{Stream stream = response2.GetResponseStream();StreamReader reader = new StreamReader(stream, encoding);string content2 = reader.ReadToEnd();return content2;}catch (WebException webEx){if (webEx.Response is HttpWebResponse errorResponse){string errorBody;using (Stream stream = errorResponse.GetResponseStream())using (StreamReader reader = new StreamReader(stream)){errorBody = reader.ReadToEnd();}return errorBody;}else{Console.WriteLine($"WebException: {webEx.Message}");return webEx.Message;}}}catch (Exception ex){ErrorMessage = ex.Message;return "";}}if (method == "POST"){Stream outstream = null;Stream instream = null;StreamReader sr = null;HttpWebResponse response = null;HttpWebRequest request = null;byte[] data = encoding.GetBytes(postData);// 准备请求...try{// 设置参数request = WebRequest.Create(url) as HttpWebRequest;CookieContainer cookieContainer = new CookieContainer();request.CookieContainer = cookieContainer;request.AllowAutoRedirect = true;request.Method = method;request.Timeout = 1000000;request.ContentType = ContentType;if (headers != null){for (int i = 0; i < headers.GetLength(0); i++){if (headers[i].Split(':').Length < 2){continue;}if (headers[i].Split(':').Length > 1){if (headers[i].Split(':')[0] == "Host"){request.Host = headers[i].Split(':')[1];continue;}else if (headers[i].Split(':')[0] == "Content-Type"){request.ContentType = headers[i].Split(':')[1];continue;}else if (headers[i].Split(':')[0] == "Connection"){request.KeepAlive = headers[i].Split(':')[1] == "close" ? false : true;continue;}else if (headers[i].Split(':')[0] == "Accept"){request.Accept = headers[i].Split(':')[1];continue;}}request.Headers.Add(headers[i]);}}request.ContentLength = data.Length;try{outstream = request.GetRequestStream();outstream.Write(data, 0, data.Length);outstream.Close();//发送请求并获取相应回应数据response = request.GetResponse() as HttpWebResponse;//直到request.GetResponse()程序才开始向目标网页发送Post请求instream = response.GetResponseStream();sr = new StreamReader(instream, encoding);//返回结果网页(html)代码string content = sr.ReadToEnd();sr.Close();sr.Dispose();return content;}catch (WebException webEx){if (webEx.Response is HttpWebResponse errorResponse){string errorBody;using (Stream stream = errorResponse.GetResponseStream())using (StreamReader reader = new StreamReader(stream)){errorBody = reader.ReadToEnd();}return errorBody;}else{Console.WriteLine($"WebException: {webEx.Message}");return webEx.Message;}}}catch (Exception ex){ErrorMessage = ex.Message;return "";}}ErrorMessage = "不正确的方法类型。(目前仅支持GET/POST)";return "";}//get response result
一些基本说明可参考我的文章:https://blog.csdn.net/michaelline/article/details/139123272?spm=1011.2415.3001.5331
非流式响应
非流式响应调用对话API 流程如下图:

调用成功,会返回类似如下JSON:
{
// 在 chat 事件里,data 字段中的 id 为 Chat ID,即会话 ID。"id": "737662","conversation_id": "737554","bot_id": "73666","completed_at": 1717508113,"last_error": {"code": 0,"msg": ""},"status": "completed","usage": {"token_count": 6644,"output_count": 766,"input_count": 5878}
}
在对话事件里,data 字段中的 id 为 Chat ID,即对话 ID,conversation_id 为会话id,这是轮询获取对话状态中需要提供的两个重要ID 参数,轮询方法 get_coze_chat_status 的说明如下表:
| 序号 | 参数名称 | 参数类型 | 说明 |
|---|---|---|---|
| 1 | paras | string | 查询参数,请进行如下字符串样例拼接即可: conversation_id=737554&chat_id=737662 |
| 2 | ApiKey | string | 申请的个人访问令牌 |
完整示例代码如下:
string ErrorMessage = "";
string ResultJson = "";
public void get_coze_chat_status(string paras,string ApiKey)
{ApiUrl = "https://api.coze.cn/v3/chat/retrieve?" + paras;WebService ws = new WebService();string[] headers = new string[2];headers[0] = "Authorization:Bearer " + ApiKey + "";headers[1] = "Content-Type:application/json";ErrorMessage = "";ResultJson = "";string rs = GetResponseResult(ApiUrl, Encoding.UTF8, "GET", "", headers);ErrorMessage = ws.ErrorMessage;ResultJson = rs;
}
当 "status" 字段值为 "completed" 的时候,表示对话处理完毕。
查询对话详情
通过扣子查看对话消息详情 API,可获取对话的最终结果,获取方法 get_coze_chat_detail 的说明如下表:
| 序号 | 参数名称 | 参数类型 | 说明 |
|---|---|---|---|
| 1 | paras | string | 查询参数,请进行如下字符串样例拼接即可: conversation_id=737554&chat_id=737662 |
| 2 | ApiKey | string | 申请的个人访问令牌 |
完整示例代码如下:
string ErrorMessage = "";
string ResultJson = "";
public void get_coze_chat_detail(string paras,string ApiKey){ApiUrl = "https://api.coze.cn/v3/chat/message/list?" + paras;WebService ws = new WebService();string[] headers = new string[2];headers[0] = "Authorization:Bearer " + ApiKey + "";headers[1] = "Content-Type:application/json";ErrorMessage = "";ResultJson = "";string rs = ws.GetResponseResult(ApiUrl, Encoding.UTF8, "GET", "", headers);ErrorMessage = ws.ErrorMessage;ResultJson = rs;}
正常返回的 JSON 比较 “庞大” ,以下是获得关键回答部分的示例代码:
string _answer="";
Newtonsoft.Json.Linq.JObject rs2 = Newtonsoft.Json.Linq.JObject.Parse(ds.ResultJson);
for(int i=0;i<rs2["data"].Count();i++){if (rs2["data"][i]["type"].ToString().ToLower() == "answer"){_answer= rs2["data"][i]["content"].ToString();return _answer;}
}
JSON对象 data 数组元素,type属性为 answer 的元素,其 content 值即为回答的内容。
小结
以上为作者初探 AI 对话的一些分享,希望与您一起探讨、交流,欢迎批评指正。
API的一些相关文档可以参考以下链接:
文档中心入口:
https://www.coze.cn/open/docs/guides
上传文件:
https://www.coze.cn/open/docs/developer_guides/upload_files
发起对话:
https://www.coze.cn/open/docs/developer_guides/chat_v3
轮询对话详情:
https://www.coze.cn/open/docs/developer_guides/retrieve_chat
查看对话消息详情:
https://www.coze.cn/open/docs/developer_guides/list_chat_messages
