尝试逐步创建“辅助审阅”工具-手札
我想构建的程序,应该是如下的:
①私域场景,也就是说,使用本地部署的大模型;
②不要离开word/wps等工具,采用将大语言模型嵌入的方式;
③需要代码开源,能够自主控制全流程。
下面,我会利用vibeCoding的方法,开展探索实践。
暂时取名叫做GOWordAgent,首先立足在windows中实现。
里程碑①在word中插入侧边栏和顶栏;(ok)
里程碑②在侧边栏中集成AI对话;(ok)
里程碑③右侧能够读取正文的内容;
里程碑④添加审阅的功能
目前全部工作已经完成,应该说对于传统程序员的我来说,有很多惊艳的地方。
【PS现有工具对于csharp的支持不是很好:

那么这里可能是需要使用vs直接集成的github copilot

所以说在代码编写领域,AI是最好地去实现的】
指令:我准备创建windows程序,在word中插入侧边栏和顶栏,顶栏的名字叫做“ GOWordAgent”。帮我分析实现路径方法。
回复:VSTO / .NET(C#)Word 外接程序(传统桌面插件,完全本地)
【这里因为我相对熟悉csharp程序,所以从最为简单的情况开始】
先把环境配好,再看代码会更顺畅。
1. Office / Windows
Word 版本:建议 Office 2016 / 2019 / 2021 / Microsoft 365(桌面版)
插件运行在 Word 进程里,你的 VSTO 插件项目一般设为 Any CPU 即可;
关键是 开发机上 Office 一定是完整安装(含“用于 .NET 的主互操作程序集”),一般默认安装就有。
2. Visual Studio
版本:建议 Visual Studio 2022 Community / Professional。

【重新恢复vs studio 程序员的工作方式,光是这个安装可能就需要相当长的时间】
安装时勾选工作负载:
“.NET 桌面开发”“Office/SharePoint 开发”(重点,没有这个就没有 VSTO 模板)

安装完成后,新建项目里应该能看到:
“Word VSTO 外接程序(Word VSTO Add-in)” 之类的模板。
3. 其他
.NET Framework:VSTO 模板通常用 .NET Framework 4.8 或类似版本,保持 VS 默认即可。
调试设置:VS 会自动把启动程序设置为 Word,按 F5 会直接启动 Word 并加载你的插件。
项目结构整体思路
创建 Word VSTO 外接程序 项目后,会有几个关键点:
ThisAddIn:插件入口,负责在 Word 启动时初始化对象(例如创建侧边栏)。
Ribbon(功能区):通过一个 Ribbon 项定义顶栏 “GOWordAgent” 和按钮。
UserControl(侧边栏 UI):WinForms 或 WPF 控件,用于放在 CustomTaskPane 中展示在右侧。
我们要实现的效果:
Word 启动时创建一个右侧隐藏的 CustomTaskPane,标题为 “GOWordAgent”。
在 Ribbon 新建标签页 GOWordAgent,放一个按钮 “显示/隐藏侧边栏”。
点击按钮时,切换右侧侧边栏的 Visible 状态。
下面给你完整的“骨架代码”,你按步骤搭一遍就能跑起来。
步骤一:创建 Word VSTO 外接程序项目
打开 Visual Studio → 创建新项目。
搜索:Word 外接程序 或 Word VSTO Add-in。
选择:“Word VSTO 外接程序” 模板 → 下一步。
填写项目名,例如:GOWordAgentAddIn → 创建。
VS 会生成一个包含 ThisAddIn.cs 的项目,并默认配置好调试用的 Word 启动。
步骤二:添加侧边栏 UserControl(GOWordAgentPane)
在项目上右键 → 添加 → 新建项。
选择 “用户控件(Windows 窗体)”(UserControl),命名为:GOWordAgentPane.cs。
在设计器里你可以先放一个简单控件,比如一个 Label 或 Button,测试用就行。
代码文件 [GOWordAgentPane.cs] 中大致是:
using System.Windows.Forms;
namespace GOWordAgentAddIn
{
public partial class GOWordAgentPane : UserControl
{
public GOWordAgentPane()
{
InitializeComponent();
}
}
}
namespace GOWordAgentAddIn
{
public partial class GOWordAgentPane : UserControl
{
public GOWordAgentPane()
{
InitializeComponent();
}
}
}
设计器自动生成的 UI 初始化代码会在 GOWordAgentPane.Designer.cs,不需要你手写。
步骤三:在 ThisAddIn 中创建 CustomTaskPane
打开 [ThisAddIn.cs],补充一个属性和 Startup 逻辑,用来创建和管理侧边栏。
using System;
using Microsoft.Office.Core;
using Microsoft.Office.Tools;
namespace GOWordAgentAddIn
{
public partial class ThisAddIn
{
internal CustomTaskPane GOWordAgentTaskPane { get; private set; }
private void ThisAddIn_Startup(object sender, EventArgs e)
{
var control = new GOWordAgentPane();
// 创建右侧的自定义任务窗格(侧边栏)
GOWordAgentTaskPane = this.CustomTaskPanes.Add(control, "GOWordAgent");
GOWordAgentTaskPane.DockPosition = MsoCTPDockPosition.msoCTPDockPositionRight;
GOWordAgentTaskPane.Width = 350; // 可以根据需要调整宽度
GOWordAgentTaskPane.Visible = false; // 默认隐藏
}
private void ThisAddIn_Shutdown(object sender, EventArgs e)
{
// 一般无需特别清理,VSTO 会处理
}
}
}
using Microsoft.Office.Core;
using Microsoft.Office.Tools;
namespace GOWordAgentAddIn
{
public partial class ThisAddIn
{
internal CustomTaskPane GOWordAgentTaskPane { get; private set; }
private void ThisAddIn_Startup(object sender, EventArgs e)
{
var control = new GOWordAgentPane();
// 创建右侧的自定义任务窗格(侧边栏)
GOWordAgentTaskPane = this.CustomTaskPanes.Add(control, "GOWordAgent");
GOWordAgentTaskPane.DockPosition = MsoCTPDockPosition.msoCTPDockPositionRight;
GOWordAgentTaskPane.Width = 350; // 可以根据需要调整宽度
GOWordAgentTaskPane.Visible = false; // 默认隐藏
}
private void ThisAddIn_Shutdown(object sender, EventArgs e)
{
// 一般无需特别清理,VSTO 会处理
}
}
}
注意:InternalStartup 方法通常在 ThisAddIn.Designer.cs 里自动生成并绑定 Startup/Shutdown 事件,正常不用改,只要确保 ThisAddIn_Startup 的签名没动即可。
步骤四:创建 Ribbon 顶栏 “GOWordAgent”
在项目右键 → 添加 → 新建项。
选择 “功能区(可视化设计器)”(Ribbon (Visual Designer)),命名为:GOWordAgentRibbon.cs。
打开 Ribbon 设计器,做以下设置:
选中最外层 tab1,右侧属性:
Label 改为:GOWordAgent
在 Ribbon 设计器中,在该 Tab 下面:
添加一个 Group(比如叫“主功能”)
在 Group 里拖入一个 Button,设置:
Name:btnTogglePane
Label:例如 “显示/隐藏侧边栏”
然后双击这个按钮,VS 会自动生成点击事件处理函数,在 [GOWordAgentRibbon.cs] 中写逻辑控制侧边栏可见性。
using Microsoft.Office.Tools.Ribbon;
namespace GOWordAgentAddIn
{
public partial class GOWordAgentRibbon
{
private void GOWordAgentRibbon_Load(object sender, RibbonUIEventArgs e)
{
}
private void btnTogglePane_Click(object sender, RibbonControlEventArgs e)
{
var pane = Globals.ThisAddIn.GOWordAgentTaskPane;
if (pane == null)
{
return; // 理论上不会发生,保险起见
}
pane.Visible = !pane.Visible;
}
}
}
namespace GOWordAgentAddIn
{
public partial class GOWordAgentRibbon
{
private void GOWordAgentRibbon_Load(object sender, RibbonUIEventArgs e)
{
}
private void btnTogglePane_Click(object sender, RibbonControlEventArgs e)
{
var pane = Globals.ThisAddIn.GOWordAgentTaskPane;
if (pane == null)
{
return; // 理论上不会发生,保险起见
}
pane.Visible = !pane.Visible;
}
}
}
上面的代码通过 Globals.ThisAddIn 拿到我们在 ThisAddIn_Startup 中创建的 CustomTaskPane,点击按钮时在 true/false 之间切换。
步骤五:运行和调试
确认 Word 已关闭。
在 VS 中设置项目为启动项目(默认即是) → 按 F5。
VS 会启动 Word 并加载你的插件:
在功能区会看到一个新的 Tab:GOWordAgent
其中有你的按钮(例如 “显示/隐藏侧边栏”)
点击按钮:
右侧应该弹出你定义的侧边栏(CustomTaskPane);
再点一次会隐藏。
如果第一次没显示:
确认 ThisAddIn_Startup 中的 GOWordAgentTaskPane 创建逻辑执行了;
确认 Ribbon 的按钮事件确实被触发(可以打断点或 MessageBox.Show 测一下)。

指令:好的,下一步我希望在打开的侧边栏中,添加一个可以用于大语言模型的对话框,并且真实调用大语言模型开展对话。
回复:这一步可以做成一个 “Word 内置的聊天窗口”。下面给你一套可以直接参考/改造的实现思路和代码示例,基于:WinForms UserControl + HttpClient 调用 OpenAI 风格接口。
一、侧边栏里要加哪些控件?
在你的 GOWordAgentPaneControl(UserControl)上,用设计器大概放这些控件(名字建议按下面来,方便对上代码):
txtConversation:TextBox
Multiline = true
ReadOnly = true
ScrollBars = Vertical
Multiline = true
ReadOnly = true
ScrollBars = Vertical
用来显示整个对话记录
txtInput:TextBox
Multiline = true(方便输入多行)
btnSend:Button
Text = "发送"
Multiline = true(方便输入多行)
btnSend:Button
Text = "发送"
Click 事件绑定到 btnSend_Click
布局随意:上面是对话记录,中间/下面是输入框,右下角一个发送按钮。
二、在 UserControl 里维护对话和调用接口
下面是一份简化版的 GOWordAgentPaneControl 代码示例,你可以把关键逻辑合并到你现有的类里(命名空间/类名按你自己的项目改):
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json; // 需要通过 NuGet 安装:Newtonsoft.Json
namespace GOWordAgentAddIn
{
public partial class GOWordAgentPaneControl : UserControl
{
private readonly HttpClient _httpClient = new HttpClient();
// 简单保存对话历史,role: "user" / "assistant"
private readonly List<(string Role, string Content)> _messages = new List<(string Role, stringContent)>();
public GOWordAgentPaneControl()
{
InitializeComponent();
// 可以根据需要设置超时
_httpClient.Timeout = TimeSpan.FromSeconds(60);
}
private async void btnSend_Click(object sender, EventArgs e)
{
var userInput = txtInput.Text.Trim();
if (string.IsNullOrEmpty(userInput))
return;
AppendToConversation("我", userInput);
_messages.Add(("user", userInput));
txtInput.Clear();
btnSend.Enabled = false;
try
{
var reply = await CallChatModelAsync();
if (!string.IsNullOrEmpty(reply))
{
_messages.Add(("assistant", reply));
AppendToConversation("GOWordAgent", reply);
}
}
catch (Exception ex)
{
AppendToConversation("系统", $"调用大模型失败:{ex.Message}");
}
finally
{
btnSend.Enabled = true;
}
}
private void AppendToConversation(string speaker, string text)
{
if (txtConversation.TextLength > 0)
txtConversation.AppendText(Environment.NewLine + Environment.NewLine);
txtConversation.AppendText($"{speaker}:{text}");
txtConversation.SelectionStart = txtConversation.TextLength;
txtConversation.ScrollToCaret();
}
/// <summary>
/// 调用大语言模型接口,示例为 OpenAI 风格的 Chat Completions 接口
/// 你可以把 URL / model / API Key 换成自己的服务
/// </summary>
private async Task<string> CallChatModelAsync()
{
// 建议用环境变量或配置文件存 API Key,这里只是示例
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
throw new InvalidOperationException("未配置 OPENAI_API_KEY 环境变量。");
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
var requestBody = new
{
model = "gpt-4o-mini", // 或你自己的模型名称
messages = BuildMessagesForApi()
};
var json = JsonConvert.SerializeObject(requestBody);
using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
using (var response = await _httpClient.PostAsync("https://api.openai.com/v1/chat/completions", content))
{
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
dynamic obj = JsonConvert.DeserializeObject(responseJson);
// OpenAI 风格返回:choices[0].message.content
string reply = obj.choices[0].message.content;
return reply?.Trim();
}
}
private List<object> BuildMessagesForApi()
{
var list = new List<object>();
foreach (var m in _messages)
{
list.Add(new
{
role = m.Role,
content = m.Content
});
}
return list;
}
}
}
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json; // 需要通过 NuGet 安装:Newtonsoft.Json
namespace GOWordAgentAddIn
{
public partial class GOWordAgentPaneControl : UserControl
{
private readonly HttpClient _httpClient = new HttpClient();
// 简单保存对话历史,role: "user" / "assistant"
private readonly List<(string Role, string Content)> _messages = new List<(string Role, stringContent)>();
public GOWordAgentPaneControl()
{
InitializeComponent();
// 可以根据需要设置超时
_httpClient.Timeout = TimeSpan.FromSeconds(60);
}
private async void btnSend_Click(object sender, EventArgs e)
{
var userInput = txtInput.Text.Trim();
if (string.IsNullOrEmpty(userInput))
return;
AppendToConversation("我", userInput);
_messages.Add(("user", userInput));
txtInput.Clear();
btnSend.Enabled = false;
try
{
var reply = await CallChatModelAsync();
if (!string.IsNullOrEmpty(reply))
{
_messages.Add(("assistant", reply));
AppendToConversation("GOWordAgent", reply);
}
}
catch (Exception ex)
{
AppendToConversation("系统", $"调用大模型失败:{ex.Message}");
}
finally
{
btnSend.Enabled = true;
}
}
private void AppendToConversation(string speaker, string text)
{
if (txtConversation.TextLength > 0)
txtConversation.AppendText(Environment.NewLine + Environment.NewLine);
txtConversation.AppendText($"{speaker}:{text}");
txtConversation.SelectionStart = txtConversation.TextLength;
txtConversation.ScrollToCaret();
}
/// <summary>
/// 调用大语言模型接口,示例为 OpenAI 风格的 Chat Completions 接口
/// 你可以把 URL / model / API Key 换成自己的服务
/// </summary>
private async Task<string> CallChatModelAsync()
{
// 建议用环境变量或配置文件存 API Key,这里只是示例
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
throw new InvalidOperationException("未配置 OPENAI_API_KEY 环境变量。");
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
var requestBody = new
{
model = "gpt-4o-mini", // 或你自己的模型名称
messages = BuildMessagesForApi()
};
var json = JsonConvert.SerializeObject(requestBody);
using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
using (var response = await _httpClient.PostAsync("https://api.openai.com/v1/chat/completions", content))
{
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
dynamic obj = JsonConvert.DeserializeObject(responseJson);
// OpenAI 风格返回:choices[0].message.content
string reply = obj.choices[0].message.content;
return reply?.Trim();
}
}
private List<object> BuildMessagesForApi()
{
var list = new List<object>();
foreach (var m in _messages)
{
list.Add(new
{
role = m.Role,
content = m.Content
});
}
return list;
}
}
}
若你用的是 OpenAI 兼容国内服务(如部分国内厂商、自己搭的代理),通常只需要改:
PostAsync 里的 URL
model 名称
有些服务把 Key 放在 Header 的其他位置,这里一起改一下 Authorization 即可。
用 Azure OpenAI 时,body 结构大体类似,但 URL 和 Header 会略有不同,可以再单独帮你改。
三、需要额外做的配置
安装 Newtonsoft.Json
在你的 VSTO 项目上右键 → “管理 NuGet 程序包”
搜索 Newtonsoft.Json → 安装
设置 API Key(以环境变量为例)
在 Windows 中添加一个用户级环境变量:OPENAI_API_KEY,值为你的密钥
重启 Visual Studio 让它能读到环境变量
不建议把 Key 写死在代码里,至少先用环境变量过渡
四、下一步可以做的增强
把和 Word 文档的交互整合进对话(例如按钮:“把当前选中文本发给大模型”)。
在 UI 上加一个下拉框选择不同模型或不同后端(OpenAI / Azure / 自建服务)。
加入简单的“系统提示”(system prompt)让 GOWordAgent 更适合你的 Word 场景(如写作助手、审稿助手等)。
如果你告诉我 你准备使用的具体大模型服务商/接口(OpenAI、Azure、阿里、月之暗面、智谱等),我可以把上面那个 CallChatModelAsync 精确改成对应的请求格式和 URL,直接给你“可粘贴”的版本。


然后貌似也是可以生成csharp程序的

请Qorder直接写的就相当不错:

那么复用这里的关键信息了?可以看到这里创建了一个专用的类。
// 调用 API
string response = await _deepSeekService.SendMessagesWithHistoryAsync(_messageHistory.ToArray());
// 显示 AI 回复
AppendMessage("DeepSeek", response, Color.Green);
// 添加到历史记录
_messageHistory.Add(new { role = "assistant", content = response });
lblStatus.Text = "状态: 就绪";
lblStatus.ForeColor = Color.Green;
string response = await _deepSeekService.SendMessagesWithHistoryAsync(_messageHistory.ToArray());
// 显示 AI 回复
AppendMessage("DeepSeek", response, Color.Green);
// 添加到历史记录
_messageHistory.Add(new { role = "assistant", content = response });
lblStatus.Text = "状态: 就绪";
lblStatus.ForeColor = Color.Green;
具体内容:
try
{
var requestBody = new
{
model = model,
messages = messages,
temperature = 0.7,
max_tokens = 2000
};
string jsonContent = JsonConvert.SerializeObject(requestBody);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
HttpResponseMessage response = await _httpClient.PostAsync(_apiUrl, content);
string responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
JObject jsonResponse = JObject.Parse(responseBody);
string? reply = jsonResponse["choices"]?[0]?["message"]?["content"]?.ToString();
return reply ?? "未获取到回复内容";
}
else
{
return $"API 调用失败: {response.StatusCode}\n{responseBody}";
}
}
catch (Exception ex)
{
return $"发生错误: {ex.Message}";
}
{
var requestBody = new
{
model = model,
messages = messages,
temperature = 0.7,
max_tokens = 2000
};
string jsonContent = JsonConvert.SerializeObject(requestBody);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
HttpResponseMessage response = await _httpClient.PostAsync(_apiUrl, content);
string responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
JObject jsonResponse = JObject.Parse(responseBody);
string? reply = jsonResponse["choices"]?[0]?["message"]?["content"]?.ToString();
return reply ?? "未获取到回复内容";
}
else
{
return $"API 调用失败: {response.StatusCode}\n{responseBody}";
}
}
catch (Exception ex)
{
return $"发生错误: {ex.Message}";
}

这样,就是在word中打通大模型了,当然现在具体功能方面还有比较多的需要优化的地方,看看copilte能否进行修改。
0117 小修改:

参考Office助手,首先是①核心要隐藏边框;②输入对话框两侧靠边,能自动伸缩;③自动完成注册。

【目前的界面当然还有一些需要修改的地方,但是应该重点推动下一步里程碑】

【里程碑③应该能够很好地去实现,未来配合代码编写工具应该是必备技能了】

【它里面很多内容写得质量是非常好的】

【但是具体用起来应该不是这样的,那么推向里程碑④,就是能够修改并且以审计的形式展现出来。我将在这里进一步完善现有的代码使其具备一定的可用价值。】
里程碑④ 修改现有的 btnFetchDoc 按钮,使其的功能为:①获取当前正文内容;②添加提示词,正文内容进行错误诊断;③以word审阅的形式反馈到原文中。

这里我是要以标注的形式进行反馈,返回json的格式是大模型自己添加的。

【这里直接以标注的形式进行展现,虽然不是我最终要的效果,但是非常接近。在每一个阶段增加对代码的阅读理解是需要的】

在打开【显示标注】的情况,这个效果应该是可行的。我决定基于此来进行下一步工作,这里还是聚焦界面显示的效果来说。

这里制作一篇存在3个错别字问题的稿子。

这里的现有的格式,就是我们选择这条路线获得的红利。那么下面开始对这里的工作进行小结:
①继续优化界面和提示词,使其具备应用价值;②完成开源部署;③整理这里的手札,形成阶段性材料。
下面开始优化界面和提示词,使其具备应用价值。要求:在进行纠错的时候,右侧需要显示出本次找到的错误。

目前是统一把需要修改的内容标黄,修改为根据severity 标注不同的颜色,low使用浅绿、 medium 使用浅黄、high使用浅红。
并且对于错误的字,直接进行修改,并且将字改成红色。【在这个地方我新开来一个copilot线程来进行处理】
修改批注的内容:①不显示 low/medium/high 字符,这个对于我来说没用;②需要显示修改前的内容,和修改后的内容;③同时给出修改的理由。
后面还有很多值得优化的地方,最直接的就是借鉴GitHub Copilot自己的实现,比如这样,来解决这里的提示:

但是在此之前,我还有另一个需要研究的问题,就是在kylin+wps上面,相关能力如何进行平替。
代码开源地址: jsxyhelu/GOWordAgentAddIn
浙公网安备 33010602011771号