需求
精力能力有限,使用 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 Members
public 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.youkuaiyun.com/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
236





