演练:使用VB开发多智能体协作的荣格八维分析器
<p data-source-line="3">在大语言模型高速发展的时代,我们面对困难的语义分析任务,通过构建智能体进行处理是一个流行趋势。本文将介绍如何使用 Visual Basic .NET 开发一个多智能体协作系统,用于分析聊天记录中特定人物的荣格八维人格类型。</p><p data-source-line="5">本文使用 CC-BY-NC-SA 4.0 协议。转载或者 AI 模型/智能体使用请注明出处并标注作者 Nukepayload2。</p>
<h2 id="背景知识" data-source-line="7">背景知识</h2>
<h3 id="什么是荣格八维" data-source-line="9">什么是荣格八维?</h3>
<p data-source-line="11">荣格八维理论是心理学家卡尔·荣格提出的认知功能理论,后发展为多个分支,其中人气较高的是 MBTI。该理论认为人的认知功能可以分为八种,在不同的位置中担任不同的原型。这些功能随着人的成长而发展,并且具有先天性。</p>
<h3 id="本文的软件工具分析的是什么" data-source-line="13">本文的软件工具分析的是什么?</h3>
<p data-source-line="14">工具分析的是对话中的人使用的人格面具。由于人的八个认知功能担任的原型是先天确定的,为了适应环境,人们会发展并使用人格面具。以树作为比喻,如果先天确定的功能偏好是树根,那么人格面具就是顶部的树枝和树叶。在对话中,人格面具以及其中的气质能被 AI 捕捉到,作为输入进行相应的分析。</p>
<h3 id="智能体协作的意义" data-source-line="16">智能体协作的意义</h3>
<p data-source-line="18">在荣格八维分析任务中,单一智能体往往难以处理所有方面。通过多个专门化智能体协作,我们可以:</p>
<ul data-source-line="19">
<li data-source-line="19">提高分析准确性</li>
<li data-source-line="20">并行处理不同维度的信息</li>
<li data-source-line="21">更好地控制分析流程</li>
<li data-source-line="22">精准提供实时反馈</li>
<li data-source-line="23">避免超长的提示词导致推理费用失控</li>
</ul>
<h2 id="需求分析" data-source-line="25">需求分析</h2>
<h3 id="目标用户场景" data-source-line="27">目标用户场景</h3>
<p data-source-line="29">想象你是一个心理学研究者,需要分析一段多人对话记录中特定人物的人格特征(使用了怎样的人格面具)。你希望系统能够:</p>
<ol data-source-line="31">
<li data-source-line="31">从聊天记录中识别目标人物的发言</li>
<li data-source-line="32">分析该人物在八维功能上的倾向性</li>
<li data-source-line="33">确定各功能在人格中的可能位置</li>
<li data-source-line="34">生成详细的分析报告</li>
<li data-source-line="35">允许在分析过程中提供补充信息</li>
<li data-source-line="36">实时查看分析过程</li>
</ol>
<h3 id="核心功能需求" data-source-line="38">核心功能需求</h3>
<ol data-source-line="40">
<li data-source-line="40"><strong>聊天记录分析</strong> - 能够解析多人对话记录</li>
<li data-source-line="41"><strong>多维度评估</strong> - 并行分析八种认知功能</li>
<li data-source-line="42"><strong>流程控制</strong> - 按照预定义顺序执行分析步骤</li>
<li data-source-line="43"><strong>实时反馈</strong> - 显示分析过程和中间结果</li>
<li data-source-line="44"><strong>交互式答疑</strong> - 分析完成后回答用户问题</li>
<li data-source-line="45"><strong>中断处理</strong> - 允许用户在分析过程中插入信息</li>
</ol>
<h2 id="系统架构设计" data-source-line="47">系统架构设计</h2>
<h3 id="整体架构" data-source-line="49">整体架构</h3>
<p data-source-line="51">系统采用主从式智能体架构:</p>
<pre class="language-text" data-role="codeBlock" data-info="{data-source-line="53"}" data-source-line="53"><code>主智能体
├── 八维得分分析器 (8个子智能体)
├── 八维位置分析器
├── 报告生成器
└── 共享工具系统
├── 笔记管理工具
├── 待办事项工具
└── 分析工具
</code></pre>
<h3 id="数据流设计" data-source-line="64">数据流设计</h3>
<ol data-source-line="66">
<li data-source-line="66"><strong>输入阶段</strong>:用户提交聊天记录和目标人物</li>
<li data-source-line="67"><strong>分析阶段</strong>:并行调用八个子智能体分析各维度</li>
<li data-source-line="68"><strong>整合阶段</strong>:综合分析结果确定功能位置</li>
<li data-source-line="69"><strong>输出阶段</strong>:生成报告并进入答疑模式</li>
</ol>
<h3 id="状态管理" data-source-line="71">状态管理</h3>
<p data-source-line="73">系统的智能体是无状态的,它们通过共享上下文(<code>MbtiAnalysisContext</code>)传递信息。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Public</span> <span style="color: rgba(0, 0, 255, 1)">Class</span><span style="color: rgba(0, 0, 0, 1)"> MbtiAnalysisContext
</span><span style="color: rgba(0, 0, 255, 1)">Public</span> <span style="color: rgba(0, 0, 255, 1)">Property</span> Note <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">New</span> ConcurrentDictionary(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">String</span>, <span style="color: rgba(0, 0, 255, 1)">String</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">Public</span> <span style="color: rgba(0, 0, 255, 1)">Property</span> TodoList <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">New</span> List(<span style="color: rgba(0, 0, 255, 1)">Of</span><span style="color: rgba(0, 0, 0, 1)"> TodoItem)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 其它内容省略</span>
<span style="color: rgba(0, 0, 255, 1)">End Class</span></pre>
</div>
<h3 id="状态管理" data-source-line="71">开发工具选择</h3>
<p>既然是自己做研究,就选自己最擅长的,最大化研究效率。</p>
<ul>
<li>VB 17.13(最新的 .NET SDK 9.x 自带)</li>
<li>Avalonia UI(使用 Nukepayload2.SourceGenerators.AvaloniaUI 提供设计时支持)</li>
<li>一些 AI 辅助编程插件</li>
</ul>
<h2 id="关键技术实现" data-source-line="83">关键技术实现</h2>
<h3 id="1-工作流控制" data-source-line="85">1. 工作流控制</h3>
<p data-source-line="87">在智能体的用户界面加载时,初始化分析器上下文并启动主循环。通过一个无限循环,系统持续运行分析任务,直到用户取消操作。当发生取消操作时,系统会根据情况重置分析器或设置中断请求标志。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Private</span> Async <span style="color: rgba(0, 0, 255, 1)">Sub</span> MbtiStatelessAnalyzerView_Loaded() <span style="color: rgba(0, 0, 255, 1)">Handles</span> <span style="color: rgba(0, 0, 255, 1)">Me</span><span style="color: rgba(0, 0, 0, 1)">.Loaded
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 设计时和重入处理(省略)</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 启动分析器</span>
<span style="color: rgba(0, 0, 0, 1)"> _analyzer.ResetContext()
</span><span style="color: rgba(0, 0, 255, 1)">Do</span>
<span style="color: rgba(0, 0, 255, 1)">Try</span><span style="color: rgba(0, 0, 0, 1)">
Await _analyzer.RunAnalysisLoopAsync(_cancelSrc.Token)
</span><span style="color: rgba(0, 0, 255, 1)">Catch</span> ex <span style="color: rgba(0, 0, 255, 1)">As</span> OperationCanceledException When ex.CancellationToken =<span style="color: rgba(0, 0, 0, 1)"> _cancelSrc.Token
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 省略取消处理</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">Try</span>
<span style="color: rgba(0, 0, 255, 1)">Loop</span>
<span style="color: rgba(0, 0, 255, 1)">End Sub</span></pre>
</div>
<p> </p>
<p data-source-line="104">智能体内采用项目管理的方式来追踪待办事项。为了确保分析按正确顺序进行,硬编码待办事项列表。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 被 ResetContext 调用</span>
<span style="color: rgba(0, 0, 255, 1)">Private</span> <span style="color: rgba(0, 0, 255, 1)">Sub</span><span style="color: rgba(0, 0, 0, 1)"> InitializeTodoList()
</span><span style="color: rgba(0, 0, 255, 1)">With</span><span style="color: rgba(0, 0, 0, 1)"> _context.TodoList
.Add(</span><span style="color: rgba(0, 0, 255, 1)">New</span> TodoItem(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">1</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">收集聊天记录和目标用户</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">...</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">))
.Add(</span><span style="color: rgba(0, 0, 255, 1)">New</span> TodoItem(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">2</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">八维分数统计</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">...</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">))
.Add(</span><span style="color: rgba(0, 0, 255, 1)">New</span> TodoItem(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">3</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">八维位置的可能性</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">...</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">))
.Add(</span><span style="color: rgba(0, 0, 255, 1)">New</span> TodoItem(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">4</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">产生报告</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">...</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">))
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">With</span>
<span style="color: rgba(0, 0, 255, 1)">End Sub</span></pre>
</div>
<p> </p>
<p data-source-line="118">主循环会按顺序处理这些任务:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Public</span> Async <span style="color: rgba(0, 0, 255, 1)">Function</span> RunAnalysisLoopAsync(cancellationToken <span style="color: rgba(0, 0, 255, 1)">As</span> CancellationToken) <span style="color: rgba(0, 0, 255, 1)">As</span><span style="color: rgba(0, 0, 0, 1)"> Task
</span><span style="color: rgba(0, 0, 255, 1)">Dim</span> result <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">String</span>
<span style="color: rgba(0, 0, 255, 1)">Do</span><span style="color: rgba(0, 0, 0, 1)">
result </span>=<span style="color: rgba(0, 0, 0, 1)"> Await ProcessMessageAsync(cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">If</span> result <> <span style="color: rgba(0, 0, 255, 1)">Nothing</span> <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
_chatHistoryItems.Add(MessageHistoryItem.CreateBotMessage(result))
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">Loop</span> <span style="color: rgba(0, 0, 255, 1)">Until</span><span style="color: rgba(0, 0, 0, 1)"> cancellationToken.IsCancellationRequested
</span><span style="color: rgba(0, 0, 255, 1)">End Function</span></pre>
</div>
<p> </p>
<p data-source-line="132">核心的任务处理逻辑在<code>ProcessMessageAsync</code>方法中实现:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Private</span> Async <span style="color: rgba(0, 0, 255, 1)">Function</span> ProcessMessageAsync(cancellationToken <span style="color: rgba(0, 0, 255, 1)">As</span> CancellationToken) <span style="color: rgba(0, 0, 255, 1)">As</span> Task(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">String</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 查找第一个未完成的任务</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> currentTask <span style="color: rgba(0, 0, 255, 1)">As</span> TodoItem = _context.TodoList.FirstOrDefault(<span style="color: rgba(0, 0, 255, 1)">Function</span>(it) <span style="color: rgba(0, 0, 255, 1)">Not</span><span style="color: rgba(0, 0, 0, 1)"> it.IsCompleted)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 处理任务</span>
<span style="color: rgba(0, 0, 255, 1)">If</span> currentTask <span style="color: rgba(0, 0, 255, 1)">IsNot</span> <span style="color: rgba(0, 0, 255, 1)">Nothing</span> <span style="color: rgba(0, 0, 255, 1)">Then</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 处理用户中断插嘴(简化)</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 根据任务类型调用相应的处理方法(简化)</span>
<span style="color: rgba(0, 0, 255, 1)">Select</span> <span style="color: rgba(0, 0, 255, 1)">Case</span><span style="color: rgba(0, 0, 0, 1)"> currentTask.Title
</span><span style="color: rgba(0, 0, 255, 1)">Case</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">收集聊天记录和目标用户</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 0, 255, 1)">Return</span><span style="color: rgba(0, 0, 0, 1)"> Await CollectChatRecordsAndTargetAsync(cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">Case</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">八维分数统计</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 0, 255, 1)">Return</span><span style="color: rgba(0, 0, 0, 1)"> Await ProcessFunctionScoresAsync(cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">Case</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">八维位置的可能性</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 0, 255, 1)">Return</span><span style="color: rgba(0, 0, 0, 1)"> Await ProcessFunctionPositionsAsync(cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">Case</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">产生报告</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 0, 255, 1)">Return</span><span style="color: rgba(0, 0, 0, 1)"> Await ProcessGenerateReportAsync(cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">Case</span> <span style="color: rgba(0, 0, 255, 1)">Else</span>
<span style="color: rgba(0, 0, 255, 1)">Return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">错误:未知任务 </span><span style="color: rgba(128, 0, 0, 1)">"</span> &<span style="color: rgba(0, 0, 0, 1)"> currentTask.Title
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">Select</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 清理用户中断插嘴(简化)</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 255, 1)">Else</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 所有任务都已完成,进入答疑状态</span>
<span style="color: rgba(0, 0, 255, 1)">Return</span><span style="color: rgba(0, 0, 0, 1)"> Await AnswerQuestionsAsync(cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">End Function</span></pre>
</div>
<p> </p>
<p data-source-line="166">子任务处理函数包括:</p>
<ol data-source-line="168">
<li data-source-line="168"><code>CollectChatRecordsAndTargetAsync</code>:负责收集用户提供的聊天记录和目标分析对象</li>
<li data-source-line="169"><code>ProcessFunctionScoresAsync</code>:调用八个并行的子智能体计算目标人物在八维功能上的得分</li>
<li data-source-line="170"><code>ProcessFunctionPositionsAsync</code>:分析各功能在人格中的可能位置</li>
<li data-source-line="171"><code>ProcessGenerateReportAsync</code>:综合所有分析结果生成最终报告</li>
<li data-source-line="172"><code>AnswerQuestionsAsync</code>:所有分析任务完成后,进入交互式答疑模式</li>
</ol>
<p data-source-line="174">这些函数会根据任务类型调用相应的工具(主要是查看笔记、调用子智能体和记录笔记),然后标记任务完成并返回结果。</p>
<h3 id="2-实时推理显示" data-source-line="176">2. 实时推理显示</h3>
<p data-source-line="178">系统通过数据绑定和事件处理机制实现实时更新。例如,在XAML视图中定义日志显示区域:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">ItemsControl</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">ItemsControl.ItemTemplate</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">DataTemplate </span><span style="color: rgba(255, 0, 0, 1)">DataType</span><span style="color: rgba(0, 0, 255, 1)">="local:MbtiAnalysisLogItem"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">Border </span><span style="color: rgba(255, 0, 0, 1)">BorderBrush</span><span style="color: rgba(0, 0, 255, 1)">="LightGray"</span><span style="color: rgba(255, 0, 0, 1)"> BorderThickness</span><span style="color: rgba(0, 0, 255, 1)">="0,0,0,1"</span><span style="color: rgba(255, 0, 0, 1)"> Padding</span><span style="color: rgba(0, 0, 255, 1)">="4"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">Expander </span><span style="color: rgba(255, 0, 0, 1)">IsExpanded</span><span style="color: rgba(0, 0, 255, 1)">="True"</span><span style="color: rgba(255, 0, 0, 1)"> HorizontalAlignment</span><span style="color: rgba(0, 0, 255, 1)">="Stretch"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">Expander.Header</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">TextBlock </span><span style="color: rgba(255, 0, 0, 1)">Text</span><span style="color: rgba(0, 0, 255, 1)">="</span><span style="color: rgba(128, 128, 0, 1)">{Binding StepName}</span><span style="color: rgba(0, 0, 255, 1)">"</span><span style="color: rgba(255, 0, 0, 1)"> FontWeight</span><span style="color: rgba(0, 0, 255, 1)">="Bold"</span><span style="color: rgba(255, 0, 0, 1)"> Foreground</span><span style="color: rgba(0, 0, 255, 1)">="Blue"</span><span style="color: rgba(0, 0, 255, 1)">/></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">Expander.Header</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">TextBlock </span><span style="color: rgba(255, 0, 0, 1)">Text</span><span style="color: rgba(0, 0, 255, 1)">="</span><span style="color: rgba(128, 128, 0, 1)">{Binding Text}</span><span style="color: rgba(0, 0, 255, 1)">"</span><span style="color: rgba(255, 0, 0, 1)"> TextWrapping</span><span style="color: rgba(0, 0, 255, 1)">="Wrap"</span><span style="color: rgba(255, 0, 0, 1)"> Margin</span><span style="color: rgba(0, 0, 255, 1)">="0,2,0,0"</span><span style="color: rgba(0, 0, 255, 1)">/></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">Expander</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">Border</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">DataTemplate</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">ItemsControl.ItemTemplate</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">ItemsControl</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p> </p>
<p data-source-line="197">用了数据绑定,智能体只要修改集合和属性,UI 就能自动更新。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">Await My.AI.Chat.ChatAsyncWithRetry(chatHistory,
</span><span style="color: rgba(0, 0, 255, 1)">Sub</span><span style="color: rgba(0, 0, 0, 1)">(response)
responseBuilder.Append(response)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 更新消息内容,这会让绑定了属性的控件显示最新的数据</span>
botMessage.Text =<span style="color: rgba(0, 0, 0, 1)"> responseBuilder.ToString()
</span><span style="color: rgba(0, 0, 255, 1)">End Sub</span><span style="color: rgba(0, 0, 0, 1)">,
cancellationToken)</span></pre>
</div>
<p> </p>
<p data-source-line="209">智能体 UI 代码还使用 Handles 子句进行消息变更事件处理,使得滚动条自动滚动到最新项:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Private</span> <span style="color: rgba(0, 0, 255, 1)">Sub</span> LogItems_CollectionChanged(sender <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">Object</span>, e <span style="color: rgba(0, 0, 255, 1)">As</span> NotifyCollectionChangedEventArgs) <span style="color: rgba(0, 0, 255, 1)">Handles</span><span style="color: rgba(0, 0, 0, 1)"> LogItems.CollectionChanged
</span><span style="color: rgba(0, 0, 255, 1)">If</span> e.Action = System.Collections.Specialized.NotifyCollectionChangedAction.Add <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
LastLogItem </span>= LogItems.ElementAtOrDefault(LogItems.Count - <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">)
AutoScrollToLastItem()
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">End Sub</span>
<span style="color: rgba(0, 0, 255, 1)">Private</span> <span style="color: rgba(0, 0, 255, 1)">Sub</span> LastLogItem_PropertyChanged(sender <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">Object</span>, e <span style="color: rgba(0, 0, 255, 1)">As</span> PropertyChangedEventArgs) <span style="color: rgba(0, 0, 255, 1)">Handles</span><span style="color: rgba(0, 0, 0, 1)"> LastLogItem.PropertyChanged
</span><span style="color: rgba(0, 0, 255, 1)">If</span> e.PropertyName = NameOf(MbtiAnalysisLogItem.Content) <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
AutoScrollToLastItem()
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">End Sub</span></pre>
</div>
<p> </p>
<p data-source-line="226">聊天栏也需要实时显示推理信息。如法炮制,不再赘述。</p>
<h3 id="3-工具系统实现" data-source-line="228">3. 工具系统实现</h3>
<p data-source-line="230">所有功能(包括传统算法和智能体)都封装为工具,通过字典进行管理:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Private</span> <span style="color: rgba(0, 0, 255, 1)">ReadOnly</span> _tools <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">New</span> Dictionary(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">String</span><span style="color: rgba(0, 0, 0, 1)">, AIFunction)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 注册工具</span>
_tools(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">record_note</span><span style="color: rgba(128, 0, 0, 1)">"</span>) = <span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> RecordNoteTool(_context)
_tools(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">view_note</span><span style="color: rgba(128, 0, 0, 1)">"</span>) = <span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> ViewNoteTool(_context)
_tools(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">function_scores</span><span style="color: rgba(128, 0, 0, 1)">"</span>) = <span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> FunctionScoresTool(_context)
_tools(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">function_positions</span><span style="color: rgba(128, 0, 0, 1)">"</span>) = <span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> FunctionPositionsTool(_context)
_tools(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">generate_report</span><span style="color: rgba(128, 0, 0, 1)">"</span>) = <span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> GenerateReportTool(_context)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> ...其它工具</span></pre>
</div>
<p> </p>
<p data-source-line="243">工具基类确保统一的调用接口(基于 Microsoft.Extensions.AI.Abstractions):</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Public</span> <span style="color: rgba(0, 0, 255, 1)">MustInherit</span> <span style="color: rgba(0, 0, 255, 1)">Class</span><span style="color: rgba(0, 0, 0, 1)"> AIFunctionBase
</span><span style="color: rgba(0, 0, 255, 1)">Inherits</span><span style="color: rgba(0, 0, 0, 1)"> AIFunction
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 模仿 WPF 的 Measure ... MeasureCore ... MeasureOverride 设计,确保与不支持 MyClass 的语言兼容,增强扩展性</span>
<span style="color: rgba(0, 0, 255, 1)">Protected</span> <span style="color: rgba(0, 0, 255, 1)">Overrides</span> <span style="color: rgba(0, 0, 255, 1)">Function</span> InvokeCoreAsync(arguments <span style="color: rgba(0, 0, 255, 1)">As</span> AIFunctionArguments, cancellationToken <span style="color: rgba(0, 0, 255, 1)">As</span> CancellationToken) <span style="color: rgba(0, 0, 255, 1)">As</span> ValueTask(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">Object</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">Return</span> <span style="color: rgba(0, 0, 255, 1)">New</span> ValueTask(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">Object</span><span style="color: rgba(0, 0, 0, 1)">)(InvokeCoreAsyncOverride(arguments, cancellationToken))
</span><span style="color: rgba(0, 0, 255, 1)">End Function</span>
<span style="color: rgba(0, 0, 255, 1)">Protected</span> <span style="color: rgba(0, 0, 255, 1)">MustOverride</span> <span style="color: rgba(0, 0, 255, 1)">Function</span> InvokeCoreAsyncOverride(arguments <span style="color: rgba(0, 0, 255, 1)">As</span> AIFunctionArguments, cancellationToken <span style="color: rgba(0, 0, 255, 1)">As</span> CancellationToken) <span style="color: rgba(0, 0, 255, 1)">As</span> Task(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">Object</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">End Class</span></pre>
</div>
<p> </p>
<p data-source-line="258">为了限制调用权限,我封装了 <code>My.AI.Chat.SetTools</code> 函数,用于控制当前智能体能用哪些工具。如有必要,子智能体也可以暴露类似的函数。</p>
<h4 id="工具调用的自由度权衡" data-source-line="260">工具调用的自由度权衡</h4>
<ul data-source-line="261">
<li data-source-line="261">对于业务已经确定的场景,提前写好代码进行强制工具调用(直接找到对应工具,调用 InvokeAsync)。这样即使模型能力不足,也能保证正确性。但是这样的弊端是限制了 AI 的创造力。</li>
<li data-source-line="262">对于需要灵活的场景,把工具列表提供给 AI,让它自己调用。坏处是不稳定。如果你能使用能力极强的模型,推荐使用这种模式。</li>
<li data-source-line="263">灵活的极致:项目管理和子智能体的调遣,以及请求用户输入,都是工具调用。开发人员只提供工具和指示。甚至允许部分子智能体的指示,以及允许使用哪些工具,都是负责管控的智能体生成的。</li>
<li data-source-line="264">稳定的极致:把工作流完全硬编码,智能体之间的任何不协调都提前写好代码处理。</li>
</ul>
<h3 id="4-中断处理机制" data-source-line="266">4. 中断处理机制</h3>
<p data-source-line="268">系统支持两种类型的中断处理机制:</p>
<ol data-source-line="270">
<li data-source-line="270"><strong>插嘴中断</strong>:用户在分析过程中提供补充信息</li>
<li data-source-line="271"><strong>完全重置</strong>:用户清空当前分析任务,重新开始</li>
</ol>
<h4 id="插嘴中断" data-source-line="273">插嘴中断</h4>
<p data-source-line="274">插嘴中断主要在<code>ProcessMessageAsync</code>方法中处理,在 <code>OnCancelInference</code> 触发:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 按钮事件处理程序</span>
<span style="color: rgba(0, 0, 255, 1)">Private</span> <span style="color: rgba(0, 0, 255, 1)">Sub</span> OnCancelInference() <span style="color: rgba(0, 0, 255, 1)">Handles</span><span style="color: rgba(0, 0, 0, 1)"> ctlChat.CancelInference
_cancelSrc.Cancel()
</span><span style="color: rgba(0, 0, 255, 1)">End Sub</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> ProcessMessageAsync 里面:</span><span style="color: rgba(0, 128, 0, 1)">
'</span><span style="color: rgba(0, 128, 0, 1)"> 处理用户中断插嘴</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> clearInterruptMessage = <span style="color: rgba(0, 0, 255, 1)">False</span>
<span style="color: rgba(0, 0, 255, 1)">If</span> UserInterruptRequested <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
UserInterruptRequested </span>= <span style="color: rgba(0, 0, 255, 1)">False</span><span style="color: rgba(0, 0, 0, 1)">
clearInterruptMessage </span>= <span style="color: rgba(0, 0, 255, 1)">True</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> interruptMsg =<span style="color: rgba(0, 0, 0, 1)"> Await UntilUserInputAsync(cancellationToken)
RecordNote(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">用户补充</span><span style="color: rgba(128, 0, 0, 1)">"</span>, interruptMsg, <span style="color: rgba(0, 0, 255, 1)">False</span><span style="color: rgba(0, 0, 0, 1)">, cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 调用工具(省略)</span><span style="color: rgba(0, 128, 0, 1)">
'</span><span style="color: rgba(0, 128, 0, 1)"> 清理用户中断插嘴的信息</span>
<span style="color: rgba(0, 0, 255, 1)">If</span> clearInterruptMessage <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
RecordNote(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">用户补充</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(0, 0, 255, 1)">String</span>.Empty, <span style="color: rgba(0, 0, 255, 1)">True</span><span style="color: rgba(0, 0, 0, 1)">, cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span></pre>
</div>
<p> </p>
<h4 id="完全重置" data-source-line="298">完全重置</h4>
<p data-source-line="299">完全重置的事件处理程序:确认要重置才会调用重置</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Private</span> Async <span style="color: rgba(0, 0, 255, 1)">Sub</span> OnClearMessageList() <span style="color: rgba(0, 0, 255, 1)">Handles</span><span style="color: rgba(0, 0, 0, 1)"> ctlChat.ClearMessageList
</span><span style="color: rgba(0, 0, 255, 1)">If</span> _pendingReset <span style="color: rgba(0, 0, 255, 1)">Then</span> <span style="color: rgba(0, 0, 255, 1)">Return</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> confirm = Await MsgBoxAsync(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">将清空当前分析任务的所有信息和日志,返回到初始状态,是否继续?</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
MsgBoxButtons.YesNo, </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">清空</span><span style="color: rgba(128, 0, 0, 1)">"</span>, TopLevel.GetTopLevel(<span style="color: rgba(0, 0, 255, 1)">Me</span><span style="color: rgba(0, 0, 0, 1)">))
</span><span style="color: rgba(0, 0, 255, 1)">If</span> confirm <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
_pendingReset </span>= <span style="color: rgba(0, 0, 255, 1)">True</span><span style="color: rgba(0, 0, 0, 1)">
_cancelSrc.Cancel()
LogItems.Clear()
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">End Sub</span></pre>
</div>
<p> </p>
<p data-source-line="314">在<code>MbtiStatelessAnalyzerView_Loaded</code>方法内的循环中,系统会根据取消原因决定是重置分析器还是设置中断请求标志:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Try</span><span style="color: rgba(0, 0, 0, 1)">
Await _analyzer.RunAnalysisLoopAsync(_cancelSrc.Token)
</span><span style="color: rgba(0, 0, 255, 1)">Catch</span> ex <span style="color: rgba(0, 0, 255, 1)">As</span> OperationCanceledException When ex.CancellationToken =<span style="color: rgba(0, 0, 0, 1)"> _cancelSrc.Token
_cancelSrc </span>= <span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> CancellationTokenSource
</span><span style="color: rgba(0, 0, 255, 1)">If</span> _pendingReset <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
_pendingReset </span>= <span style="color: rgba(0, 0, 255, 1)">False</span><span style="color: rgba(0, 0, 0, 1)">
_analyzer.ResetContext()
</span><span style="color: rgba(0, 0, 255, 1)">Else</span><span style="color: rgba(0, 0, 0, 1)">
_analyzer.UserInterruptRequested </span>= <span style="color: rgba(0, 0, 255, 1)">True</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">Try</span></pre>
</div>
<p> </p>
<h3 id="5-答疑模式实现" data-source-line="330">5. 答疑模式实现</h3>
<p data-source-line="332">分析完成后,系统进入交互式答疑模式。这其实就是个普通的对话智能体,它不仅拥有分析报告和原始数据,还能自己决定调用哪些工具来补充上下文(比如查看笔记):</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Private</span> Async <span style="color: rgba(0, 0, 255, 1)">Function</span> AnswerQuestionsAsync(cancellationToken <span style="color: rgba(0, 0, 255, 1)">As</span> CancellationToken) <span style="color: rgba(0, 0, 255, 1)">As</span> Task(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">String</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 限制工具集,只允许答疑相关的工具</span>
<span style="color: rgba(0, 0, 0, 1)"> My.AI.Chat.SetTools(_qaTools)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 构建初始上下文消息(简化)</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> contextMessage <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">String</span> = $<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">以下是你之前提供的聊天记录和分析结果:</span>
<span style="color: rgba(0, 0, 0, 1)">用户:{targetSpeaker}
聊天记录:{chatRecords}
八维分数统计结果:{scoreResults}
八维位置分析结果:{positionResults}
最终报告:{report}</span><span style="color: rgba(128, 0, 0, 1)">"
</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 一问一答循环。简化的代码是:</span>
<span style="color: rgba(0, 0, 255, 1)">Do</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> message <span style="color: rgba(0, 0, 255, 1)">As</span> ChatMessage =<span style="color: rgba(0, 0, 0, 1)"> Await UntilUserInputAsync(cancellationToken)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 获取用户问题内容</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> question <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">String</span> =<span style="color: rgba(0, 0, 0, 1)"> GetMessageContent(message)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 将用户问题添加到聊天历史</span>
chatHistory.Add(<span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> ChatMessage(ChatRole.User, question))
</span><span style="color: rgba(0, 0, 255, 1)">Dim</span> responseBuilder <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> StringBuilder
Await My.AI.Chat.ChatAsyncWithRetry(chatHistory,
</span><span style="color: rgba(0, 0, 255, 1)">Sub</span><span style="color: rgba(0, 0, 0, 1)">(response)
responseBuilder.Append(response)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 更新消息内容</span>
botMessage.Text =<span style="color: rgba(0, 0, 0, 1)"> responseBuilder.ToString()
</span><span style="color: rgba(0, 0, 255, 1)">End Sub</span><span style="color: rgba(0, 0, 0, 1)">,
cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">Dim</span> answer <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">String</span> =<span style="color: rgba(0, 0, 0, 1)"> responseBuilder.ToString()
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 将助手回答添加到聊天历史</span>
chatHistory.Add(<span style="color: rgba(0, 0, 255, 1)">New</span><span style="color: rgba(0, 0, 0, 1)"> ChatMessage(ChatRole.Assistant, answer))
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 通知用户输入(确保界面更新)</span>
<span style="color: rgba(0, 0, 0, 1)"> _requestUserInput()
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 循环继续,等待用户下一次提问</span>
<span style="color: rgba(0, 0, 255, 1)">Loop</span>
<span style="color: rgba(0, 0, 255, 1)">End Function</span></pre>
</div>
<p> </p>
<h2 id="一些技术细节" data-source-line="379">一些技术细节</h2>
<h3 id="1-并行处理优化" data-source-line="381">1. 并行处理优化</h3>
<p data-source-line="383">在分析八维得分时,系统并行调用八个子智能体:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 创建任务数组并行化调用每个分析器的 ProcessMessageAsync 方法</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> tasks <span style="color: rgba(0, 0, 255, 1)">As</span> Task(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">Integer</span>)() =<span style="color: rgba(0, 0, 0, 1)"> {
niAgent.ProcessMessageAsync(personToAnalysis, messagesToAnalysis, reportLog, cancellationToken),
neAgent.ProcessMessageAsync(personToAnalysis, messagesToAnalysis, reportLog, cancellationToken),
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> ... 其他五个智能体</span>
<span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 等待所有任务完成</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> results <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">Integer</span>() = Await Task.WhenAll(tasks)</pre>
</div>
<p> </p>
<p data-source-line="397">这种设计充分利用推理 API 并发的特性,显著缩短分析时间。</p>
<h3 id="2-错误处理与重试机制" data-source-line="399">2. 错误处理与重试机制</h3>
<p data-source-line="401">每个子智能体都实现了重试机制,确保分析的可靠性:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Dim</span> retryCount <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">Integer</span> = <span style="color: rgba(128, 0, 128, 1)">0</span>
<span style="color: rgba(0, 0, 255, 1)">Do</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 调用AI分析</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> ... 使用 My.AI.Chat.ChatAsyncWithRetry</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 分析字符串以取得 score 的值</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 验证结果有效性</span>
<span style="color: rgba(0, 0, 255, 1)">If</span> score >= <span style="color: rgba(128, 0, 128, 1)">0</span> <span style="color: rgba(0, 0, 255, 1)">AndAlso</span> score <= <span style="color: rgba(128, 0, 128, 1)">100</span> <span style="color: rgba(0, 0, 255, 1)">Then</span>
<span style="color: rgba(0, 0, 255, 1)">Exit</span> <span style="color: rgba(0, 0, 255, 1)">Do</span> <span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 成功提取有效分数,退出循环</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 增加重试计数</span>
retryCount += <span style="color: rgba(128, 0, 128, 1)">1</span>
<span style="color: rgba(0, 0, 255, 1)">If</span> retryCount >= maxRetries <span style="color: rgba(0, 0, 255, 1)">Then</span>
<span style="color: rgba(0, 0, 255, 1)">Throw</span> <span style="color: rgba(0, 0, 255, 1)">New</span> AILowIQException(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">重试次数过多,请检查AI模型是否正确配置</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">Loop</span></pre>
</div>
<p> </p>
<p data-source-line="424">对于与推理服务的交互,我封装了 <code>My.AI.Chat.ChatAsyncWithRetry</code>,它也做了错误分类和重试。</p>
<h3 id="3-ui线程安全更新" data-source-line="426">3. UI线程安全更新</h3>
<p data-source-line="428">通过Dispatcher确保UI更新在正确的线程上执行:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">Private</span> <span style="color: rgba(0, 0, 255, 1)">Sub</span> ReportLog(logItem <span style="color: rgba(0, 0, 255, 1)">As</span><span style="color: rgba(0, 0, 0, 1)"> MbtiAnalysisLogItem)
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 在UI线程上更新日志列表</span>
<span style="color: rgba(0, 0, 0, 1)"> Avalonia.Threading.Dispatcher.UIThread.Post(
</span><span style="color: rgba(0, 0, 255, 1)">Sub</span><span style="color: rgba(0, 0, 0, 1)">()
LogItems.Add(logItem)
</span><span style="color: rgba(0, 0, 255, 1)">End Sub</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">End Sub</span></pre>
</div>
<p> </p>
<h3 id="4-上下文清洗过滤" data-source-line="440">4. 上下文清洗过滤</h3>
<p data-source-line="441">这一步对于稳定性至关重要。对于用户输入,要通过子智能体挑选有效的输入数据,并确保输入数据符合要求。另外,如果上下文过长需要压缩,用这种办法也能减少数据丢失。</p>
<p data-source-line="443">例如,在<code>CollectChatRecordsAndTargetAsync</code>函数中,实现了独立的智能体,它辅助验证用户输入的内容是否为有效的聊天记录格式,并提取要分析的目标用户名称。这种方法确保了后续分析步骤能够获得正确的数据,避免了因输入数据格式不正确或不完整而导致的分析错误。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 代码经过简化,只展示核心逻辑</span>
<span style="color: rgba(0, 0, 255, 1)">Private</span> Async <span style="color: rgba(0, 0, 255, 1)">Function</span> CollectChatRecordsAndTargetAsync(cancellationToken <span style="color: rgba(0, 0, 255, 1)">As</span> CancellationToken) <span style="color: rgba(0, 0, 255, 1)">As</span> Task(<span style="color: rgba(0, 0, 255, 1)">Of</span> <span style="color: rgba(0, 0, 255, 1)">String</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">Dim</span> chatRecordOk = <span style="color: rgba(0, 0, 255, 1)">False</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> analysisTargetOk = <span style="color: rgba(0, 0, 255, 1)">False</span>
<span style="color: rgba(0, 0, 255, 1)">Do</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> message <span style="color: rgba(0, 0, 255, 1)">As</span> ChatMessage =<span style="color: rgba(0, 0, 0, 1)"> Await UntilUserInputAsync(cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">Dim</span> messageContent <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">String</span> =<span style="color: rgba(0, 0, 0, 1)"> GetMessageContent(message)
</span><span style="color: rgba(0, 0, 255, 1)">If</span> <span style="color: rgba(0, 0, 255, 1)">Not</span> chatRecordOk <span style="color: rgba(0, 0, 255, 1)">Then</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 调用 AI 判断是否是聊天记录</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> isChatRecord <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">Boolean</span> =<span style="color: rgba(0, 0, 0, 1)"> Await IsChatRecordAsync(message, cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">If</span> isChatRecord <span style="color: rgba(0, 0, 255, 1)">Then</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 记录用户的聊天记录到笔记中</span>
RecordNote(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">聊天记录</span><span style="color: rgba(128, 0, 0, 1)">"</span>, messageContent, <span style="color: rgba(0, 0, 255, 1)">True</span><span style="color: rgba(0, 0, 0, 1)">, cancellationToken)
chatRecordOk </span>= <span style="color: rgba(0, 0, 255, 1)">True</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">If</span> <span style="color: rgba(0, 0, 255, 1)">Not</span> analysisTargetOk <span style="color: rgba(0, 0, 255, 1)">Then</span>
<span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 提取分析目标</span>
<span style="color: rgba(0, 0, 255, 1)">Dim</span> analysisTarget <span style="color: rgba(0, 0, 255, 1)">As</span> <span style="color: rgba(0, 0, 255, 1)">String</span> =<span style="color: rgba(0, 0, 0, 1)"> Await ExtractAnalysisTargetAsync(messageContent, cancellationToken)
</span><span style="color: rgba(0, 0, 255, 1)">If</span> <span style="color: rgba(0, 0, 255, 1)">Not</span> <span style="color: rgba(0, 0, 255, 1)">String</span>.IsNullOrEmpty(analysisTarget) <span style="color: rgba(0, 0, 255, 1)">Then</span><span style="color: rgba(0, 0, 0, 1)">
RecordNote(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">分析目标</span><span style="color: rgba(128, 0, 0, 1)">"</span>, analysisTarget, <span style="color: rgba(0, 0, 255, 1)">True</span><span style="color: rgba(0, 0, 0, 1)">, cancellationToken)
analysisTargetOk </span>= <span style="color: rgba(0, 0, 255, 1)">True</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">End</span> <span style="color: rgba(0, 0, 255, 1)">If</span>
<span style="color: rgba(0, 0, 255, 1)">Loop</span> <span style="color: rgba(0, 0, 255, 1)">Until</span> chatRecordOk <span style="color: rgba(0, 0, 255, 1)">AndAlso</span><span style="color: rgba(0, 0, 0, 1)"> analysisTargetOk
</span><span style="color: rgba(0, 128, 0, 1)">'</span><span style="color: rgba(0, 128, 0, 1)"> 其它逻辑</span>
<span style="color: rgba(0, 0, 255, 1)">End Function</span></pre>
</div>
<p> </p>
<h2 id="总结" data-source-line="478">总结</h2>
<p data-source-line="480">通过这个项目,我展示了如何使用 Visual Basic .NET 构建一个复杂的多智能体协作系统。关键设计决策包括:</p>
<ol data-source-line="482">
<li data-source-line="482"><strong>强制工作流</strong>:通过硬编码待办事项列表确保分析按预期顺序进行,部分子智能体会强制调用指定的工具</li>
<li data-source-line="483"><strong>具有图形界面</strong>:通过事件处理机制提供分析过程的实时可视化</li>
<li data-source-line="484"><strong>工具化设计</strong>:将所有功能封装为工具,便于管理和扩展</li>
<li data-source-line="485"><strong>无状态智能体</strong>:通过共享上下文实现智能体间的数据传递</li>
<li data-source-line="486"><strong>实时交互体验</strong>:提供中断处理和答疑模式,增强交互性</li>
</ol>
<p data-source-line="488">这个系统不仅能够准确分析人格类型,还提供了良好的可解释性和用户体验。这种设计方式也能套用到其它智能体,而不仅局限于荣格八维分析。</p>
<p data-source-line="490">本文使用 CC-BY-NC-SA 4.0 协议。转载或者 AI 模型/智能体使用请注明出处并标注作者 Nukepayload2。</p><br><br>
来源:https://www.cnblogs.com/Nukepayload2/p/19020713
頁:
[1]