开源项目解读:Microsoft Multi-Modal Customer Service Agent

项目定位:Microsoft 官方发布的生产级 Solution Accelerator,展示如何在 Azure 上构建一个支持语音/电话/文字的多域 AI 客服系统,集成多 Agent 编排、实时流式推理、RAG 知识检索与完整可观测性。

整体代码库

引言

plain 复制代码
## 输入 / 输出梳理
- 输入:用户的实时语音流(PCM16 音频帧,base64编码),通过 WebSocket 推送
- 输出:AI Agent 的实时语音回复(PCM16 音频帧流)+ 文字转录 + 工具调用结果(订票/改签等实际业务操作)

## 核心难点分析

### 难点1:多 Agent 无缝切换
语音对话中,用户在同一通话里可能从"酒店预订"跳到"机票查询"。
传统 chatbot 做法是让 LLM 自己判断该叫哪个 agent,但这会把这个判断负担加到主模型上,
延迟高、容易出错。

作者的 Engineering Trick:
用一个独立的 "分类专用小模型"(GPT-4o-mini 或 fine-tuned 分类器)来做 detect_intent(),
将"意图路由"和"对话生成"完全解耦。小模型 max_tokens=20,极其轻量,不影响主模型 latency。
意图切换后通过 update_session() 热替换 kernel+instructions,不断连接、不重建 WebSocket。

### 难点2:实时语音流与 Agent 工具调用的并发协调
音频流是持续推送的,但 Agent 可能正在执行 DB 查询或向量搜索。
如果不加锁,agent 回复音频帧和工具调用结果可能乱序。

作者的 Trick:
active_response flag + asyncio.gather 双协程模式。
收到 input_audio_transcription.completed 后才触发 response.create,
保证"先完成语音识别→再做意图检测→再生成回复"的严格时序。

### 难点3:水平扩展下的会话状态持久化
多副本部署时,同一 session_key 可能落到不同 Pod。
作者用 SessionState 类做了优雅的 双模式(in-memory/Redis)无缝切换,
Chat History 以 pickle+base64 的方式存 Redis,反序列化后直接喂给 ChatHistoryTruncationReducer。

### 难点4:知识库防幻觉
Persona YAML 里明确约束:"Do not generate answers that are not based on the search information."
工具层做了向量语义搜索,LLM 只能 cite 工具返回的内容,形成自然的 RAG 防幻觉闭环。

### 合理推测(原文未完全揭示的部分)
- hotel_policy.json 文件 13MB+,内部应是 pre-computed embedding 的 JSON 数组
  (每条 { id, policy_text, policy_text_embedding })
- fine-tuned intent 分类模型部署在 Azure ML Online Endpoint(有专用的 INTENT_SHIFT_API_URL/KEY/DEPLOYMENT 三元素)
- 训练数据生成采用 LLM-as-data-generator(用 GPT-4 生成 3253 条标注样本),这是典型的 synthetic data 方案LLM

项目全局视角

业务痛点

传统客服系统面临三大天花板:

痛点 传统方案的局限 本方案如何突破
跨域问题跳转 客户从酒店问题问到机票问题,需要人工转接 实时意图检测,毫秒级无感切换 AI Agent
幻觉与合规风险 LLM 可能编造政策内容 强制 RAG:Agent 只能引用工具返回内容
实时语音延迟 TTS+ASR 两跳带来 >1s 延迟 Azure OpenAI GPT-4o Realtime API,端到端 <200ms
水平扩展 Session 状态绑定单机 Redis 分布式 Session Store,支持无状态横扩

"胜负手"三件套

  1. GPT-4o Realtime WebSocket:语音直接进模型**,省去 ASR→文本→TTS 三跳,**是整个方案低延迟的根基。
  2. 独立意图分类器:将"路由判断"从主模型解耦,是多 Agent 无缝切换的核心 Engineering Trick。
  3. YAML 驱动的 Agent 人格模板 :用 {customer_name} / {customer_id} 占位符在运行时注入上下文,实现**"零改代码新增 Agent"**的可扩展性。

多模态数据摄入与解析 (Data Ingestion & Parsing)

语音流的"解析":PCM16 实时帧

这个方案中的"多模态"核心是语音模态。数据摄入链路如下:

What(是什么) :用户麦克风采集的原始音频,以 PCM16 格式(16-bit 有符号整数,单声道)逐帧 base64 编码 后通过 WebSocket 推送。

Why(为什么):PCM16 是最原始的无损格式,无需解码开销;base64 编码让二进制数据在 JSON 文本帧中安全传输;GPT-4o Realtime API 原生支持此格式。

How(怎么实现)

python 复制代码
# frontend/src/hooks/useRealtime.tsx --- 浏览器端音频采集与发送
const addUserAudio = (base64Audio: string) => {
    const command: InputAudioBufferAppendCommand = {
        type: "input_audio_buffer.append",
        audio: base64Audio   // PCM16 帧, base64 编码
    };
    sendJsonMessage(command);
};
python 复制代码
# rtmt.py --- 后端接收并转发给 Azure OpenAI Realtime
elif msg_type == SendEvents.INPUT_AUDIO_BUFFER_APPEND:
    audio_data = message.get("audio")
    if audio_data:
        await realtime_client.send(
            event=RealtimeAudioEvent(
                audio=AudioContent(
                    data=audio_data, data_format="base64"),
            )
        )

关键参数配置 :Turn Detection 使用 **server_vad**(服务端语音活动检测),阈值 0.5,前置填充 300ms,静音截断 200ms。这意味着用户说完话 200ms 后自动触发推理,极大降低感知延迟。

电话信道的特殊接入:ACS Bridge

What :Azure Communication Services(ACS)的电话信道传来的是 PCM24K_MONO(24kHz 单声道)格式的混合音频流。

Why :电话网络的 PSTN 信号与 WebSocket 完全异构,需要一个专用的 Bridge 层来适配协议差异。

How(ACS 桥接器 acs_realtime.py 的核心逻辑)

python 复制代码
# 1. ACS 通过 EventGrid 回调通知有来电
# 2. Bridge 应答并建立媒体流 WebSocket(以 callerId 为 session key)
# 3. 双向桥接:ACS音频 → realtime 服务 / realtime回复 → ACS

async def forward_acs_to_realtime():
    # ACS 消息格式:{"kind": "AudioData", "audioData": {"data": "<base64>"}}
    # 转换成 realtime 格式:{"type": "input_audio_buffer.append", "audio": "<base64>"}
    ...

async def forward_realtime_to_acs():
    # 关键:检测到 "input_audio_buffer.speech_started" 时,
    # 向 ACS 发送 StopAudio 信号实现"打断"(barge-in)
    if message.get("type") == "input_audio_buffer.speech_started":
        await websocket.send(json.dumps(
            {"Kind": "StopAudio", "AudioData": None, "StopAudio": {}}
        ))

工程亮点 :Barge-in(用户打断 AI 说话 )的实现非常精妙------当检测到用户开始说话(speech_started 事件),立即向 ACS 发 StopAudio 指令,停止正在播放的 AI 音频,实现真正自然的打断体验。

知识库的预计算嵌入(RAG 数据摄入)

What :酒店/航班政策知识库以 hotel_policy.json / flight_policy.json 存储,内部是预计算好的 embedding 数组:

json 复制代码
[
  {
    "id": "hotel_policy_001",
    "policy_text": "Check-in time is 3:00 PM...",
    "policy_text_embedding": [0.0123, -0.0456, ...]  // 1536-dim vector
  },
  ...
]

Why:在线 query 时只需要对 query 做一次 embedding,然后与预计算的向量做余弦相似度计算,不需要实时 embed 所有文档,延迟极低。

How(嵌入搜索实现)

python 复制代码
# hotel_plugins.py --- 极简但高效的向量检索实现
class SearchClient:
    def __init__(self, emb_map_file_path: str):
        with open(emb_map_file_path) as file:
            self.chunks_emb = json.load(file)  # 一次性加载全部预计算向量到内存

    def find_article(self, question: str, topk: int = 3) -> str:
        input_vector = get_embedding(question)  # 仅对 query 做一次在线 embedding
        cosine_list = [
            (item['id'], item['policy_text'],
             1 - spatial.distance.cosine(input_vector, item['policy_text_embedding']))
            for item in self.chunks_emb
        ]
        cosine_list.sort(key=lambda x: x[2], reverse=True)
        return "\n".join(f"{chunk_id}\n{content}" 
                         for chunk_id, content, _ in cosine_list[:topk])

切块策略(Chunking) :从文件大小(~13MB)和结构推断,每个 policy_text 对应一条政策条款(段落级),粒度适中------过细会损失上下文,过粗会引入噪声。这是典型的**固定语义段落切分**策略。


检索与召回引擎 (Retrieval & Reranking)

检索方案:纯向量语义检索

What :使用 Azure OpenAI Embedding 模型(text-embedding-ada-002text-embedding-3-small)将 query 映射为向量,用 scipy.spatial.distance.cosine 计算余弦距离,top-3 召回。

Why

  • 政策问答的 query 通常是自然语言("宠物政策是什么"),语义检索比关键词检索更鲁棒。
  • 知识库规模小(政策条款有限),暴力全量余弦计算(O(n))完全够用,无需 ANN 索引。
  • 不引入 BM25 的原因:政策文本通常没有严格的关键词依赖,纯语义检索足够精准。

How :如上 SearchClient.find_article() 实现。

生产扩展建议 (文档中明确提及):当知识库规模增长,迁移到 Azure AI Search / Pinecone / Qdrant,无需改动上层 Agent 调用逻辑,因为知识检索被完全封装在 @kernel_function 工具中。

Reranker 的缺失与其合理性

本方案未使用独立的 Reranker,理由充分:

  • Top-3 召回的文档会全部拼接送入 GPT-4o,GPT-4o 本身在处理 3 个候选段落时具备足够的"内置 rerank"能力(注意力机制会自然聚焦最相关内容)。
  • 政策 QA 场景的召回精度本身较高****,引入独立 Reranker 收益边际较小,反而增加延迟。


双路数据访问:向量 + SQL

这是本方案检索层最值得关注的设计------两种完全不同的检索范式共存于同一 Agent

检索类型 用途 工具函数 返回方式
向量语义检索 政策/FAQ 问答 search_hotel_knowledgebase Top-3 文本片段
精确SQL查询 订单/预订/航班状态 load_user_reservation_info 结构化 JSON

两者的协同 :Agent 接到政策问题 → 调 search_*_knowledgebase接到订单问题 → 调 SQL 工具 。GPT-4o 的 Function Calling 能力负责判断"什么问题用什么工具",这本质上是一个轻量级的 Intent-to-Tool 路由


Agent 编排与防幻觉

三层 Agent 架构

plain 复制代码
┌─────────────────────────────────────────────────┐
│  Layer 1: 意图路由层(Router)                    │
│  detect_intent() → 分类模型/GPT-4o-mini          │
│  职责:判断当前对话属于哪个领域                    │
└────────────────────┬────────────────────────────┘
                     │ 触发 agent 切换
┌────────────────────▼────────────────────────────┐
│  Layer 2: Domain Agent 层(Hotel/Flight/...)    │
│  每个 Agent 有独立 SK Kernel + YAML Persona      │
│  职责:领域内的对话管理与工具调用决策              │
└────────────────────┬────────────────────────────┘
                     │ @kernel_function 调用
┌────────────────────▼────────────────────────────┐
│  Layer 3: 工具执行层(Tools)                    │
│  SQL 操作 / 向量检索 / 外部 API                  │
│  职责:原子化业务操作,返回结构化数据              │
└─────────────────────────────────────────────────┘

意图路由机制深度拆解

Whatdetect_intent() 函数,对每一个用户 utterance(语音识别完成后)触发一次意图分类推断。

Why 要做成独立分类器而不让主模型自己判断

  1. 延迟优势 :GPT-4o-mini max_tokens=20 的推断速度远快于完整的 GPT-4o 对话推理
  2. 精确性:专门的分类模型(fine-tuned)比 zero-shot 提示工程更稳定
  3. 解耦:路由逻辑与对话逻辑完全分离,互不干扰

双模式实现(生产 vs 开发)

python 复制代码
# utility.py --- detect_intent 的双模式实现
async def detect_intent(conversation):
    if INTENT_SHIFT_API_URL:
        # 模式1:调用 Azure ML Online Endpoint 上的 fine-tuned 分类器
        # 输入:对话文本(字符串)
        # 输出:agent 名称字符串(如 "hotel_agent" / "flight_agent")
        data = {
            "input_data": {
                "columns": ["input_string"],
                "index": [0],
                "data": [[conversation]]   # AzureML 推理服务的标准输入格式
            },
            "params": {}
        }
        # ...HTTP 调用...
    else:
        # 模式2:GPT-4o-mini 零样本分类(开发环境 fallback)
        messages = [
            {"role": "system", "content": "You are a classifier model...
             只能回复 hotel_agent 或 flight_agent"},
            {"role": "user", "content": conversation}
        ]
        response = await async_client.chat.completions.create(
            model=AZURE_OPENAI_4O_MINI_DEPLOYMENT,
            messages=messages,
            max_tokens=20    # 强制模型只输出一个词,极省 token
        )

意图切换的状态机

Agent 切换的整个流程是一个精心设计的状态机,核心状态字段如下:

python 复制代码
session = {
    "current_agent": ...,          # 当前服务的 Agent 对象
    "current_agent_kernel": ...,   # 当前 Agent 的 SK Kernel(含工具集)
    "target_agent_name": None,     # 检测到的目标 Agent 名称(切换前暂存)
    "transfer_conversation": False, # 是否正在切换
    "active_response": False,       # 是否有正在生成的回复(防并发冲突)
}

关键时序(以用户从酒店问题切到机票问题为例):

plain 复制代码
用户说: "另外,我的机票能改签吗?"
    ↓
[Whisper 转录完成] → CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_COMPLETED 事件
    ↓
session["history"].add_user_message(transcript)  # 先记录历史
    ↓
await self._detect_intent_change(session)         # 异步意图检测
    ↓  (检测到 intent="flight_agent" ≠ current="hotel_agent")
session["target_agent_name"] = "flight_agent"
session["transfer_conversation"] = True
    ↓
await self._reinitialize_session(realtime_client, session)
    ├── 发送 input_audio_buffer.clear(清空音频缓冲,防止残留音频触发旧Agent)
    ├── 切换 current_agent 和 current_agent_kernel
    ├── 用新 Agent 的 persona 格式化 instructions(注入客户姓名/ID)
    └── await realtime_client.update_session(settings, kernel=新Kernel)
    ↓
await realtime_client.send(RealtimeEvent("response.create"))  # 用新Agent生成回复

防重复响应保护 :代码中的 active_response flag 确保在已有一个响应正在生成时,不会重复触发 response.create,避免 Agent 说话叠加。

防幻觉机制的三道防线

第一道:Persona YAML 的硬约束指令

yaml 复制代码
# hotel_agent_profile.yaml
- Provide answers based solely on the facts from the search tool.
  If there isn't enough information, inform the customer that you don't know.
- Do not generate answers that are not based on the search information.

这是一个写在 System Prompt 级别的"宪法约束",GPT-4o 接受指令训练,对此类约束服从度很高。

第二道:工具调用强制接地(Grounding)

python 复制代码
# SK 的 FunctionChoiceBehavior.Auto() 设置
# 让模型在"有工具可用时优先调用工具"
function_choice_behavior=FunctionChoiceBehavior.Auto()

Agent 被训练成"先工具后回答"的行为模式:查政策必须先调 search_knowledgebase,查订单必须先调 load_user_*_info

第三道:ChatHistory 截断防止上下文污染

python 复制代码
# 只保留最近 3 轮对话
max_history_length = 3
init_history = ChatHistoryTruncationReducer(target_count=self.max_history_length)

过长的历史会引入早期的幻觉内容或错误信息,截断到 3 轮可以在保证对话连贯性的同时,防止**"历史污染"**。

训练数据生成(意图分类器)

这部分是整个方案中最被低估的工程亮点之一。

What :用 GPT-4(temperature=0.9,JSON 强制输出模式)批量合成 3253 条意图分类标注数据。

How(数据格式设计的巧妙之处)

python 复制代码
# training_data_gen.ipynb --- 训练数据的两阶段设计

# 第一阶段:生成带 current_domain + intent_shift 的样本
{
    "conversation_transcript": ["agent: ...", "user: ..."],
    "current_domain": "flight_agent",
    "intent_shift": "hotel_agent"  # 或 "no_change"
}

# 第二阶段:转换为纯分类任务格式(去掉 current_domain,直接预测最终 intent)
{
    "conversation": "agent: ...\nuser: ...",
    "intent": "hotel_agent"  # no_change → 转换为实际的 current_domain
}

关键的 Label 设计决策 :将 no_change 转换为 current_domain 的名字(比如 hotel_agent),让模型学会的是"当前应该由哪个 Agent 服务",而不是学"是否发生了切换"------这是一个更鲁棒的 formulation,避免了 binary classification 的局限性。

数据分布控制

python 复制代码
prompt = """This time, I need to generate a lot more examples in "no_change", 
"general_agent" and "car_rental_agent" categories so focus only on these."""

作者注意到类别不平衡问题,通过在 prompt 中明确指定需要更多少数类样本来解决------这是 synthetic data 生成的标准技巧。

数据验证管道

python 复制代码
for d in data['training_data']:
    # 四重验证:字段完整性 + 标签合法性 + domain 合法性
    if ("conversation_transcript" in d and 
        "current_domain" in d and 
        "intent_shift" in d and 
        (d["intent_shift"] in agents or d["intent_shift"] == "no_change") and 
        d["current_domain"] in agents):
        output_data.append(d)

全栈技术组件详解

Semantic Kernel 的角色

What :Microsoft 开源的 AI Orchestration SDK,在本项目中承担三个职责:

  1. 封装 Azure OpenAI Realtime WebSocket 连接(AzureRealtimeWebsocket
  2. 管理 @kernel_function 工具注册与 Function Calling 执行链
  3. 提供 ChatHistoryTruncationReducer 用于历史截断

Why 选 SK 而不是 LangChain:项目是 Microsoft 官方出品,SK 是 Microsoft 自家的框架,与 Azure OpenAI 的集成更紧密,特别是 Realtime API 的支持在 SK 中是一等公民。

会话状态存储:优雅的双模式降级

python 复制代码
# utility.py --- SessionState 类:优雅的生产/开发双模式
class SessionState:
    def __init__(self):
        AZURE_REDIS_ENDPOINT = os.getenv("AZURE_REDIS_ENDPOINT")
        AZURE_REDIS_KEY = os.getenv("AZURE_REDIS_KEY")
        if AZURE_REDIS_KEY:
            # 生产模式:Redis(SSL 连接,端口 6380)
            self.redis_client = redis.StrictRedis(
                host=AZURE_REDIS_ENDPOINT, port=6380, 
                password=AZURE_REDIS_KEY, ssl=True)
        else:
            # 开发模式:内存字典
            self.session_store: Dict[str, Dict] = {}

    def set(self, key, value):
        if self.redis_client:
            # pickle 序列化 + base64 编码,使 Python 对象可存入 Redis
            self.redis_client.set(key, base64.b64encode(pickle.dumps(value)))
        else:
            self.session_store[key] = value

设计模式洞察:这是**"Strategy Pattern + Graceful Degradation"**的完美结合。通过环境变量切换后端存储,上层代码零改动。

pickle+base64 的序列化方式虽然不如 JSON 安全,但能完整保存 ChatHistoryTruncationReducer 对象的所有状态(包括内部的 messages 列表和截断配置)。

可观测性:OpenTelemetry 全链路

本项目的可观测性设计是工业级水准,支持三种导出模式的动态组合:

python 复制代码
# rtmt.py --- 通过环境变量 TELEMETRY_SCENARIO 控制
# 支持逗号分隔的多场景:如 "application_insights,console"
TELEMETRY_SCENARIOS = os.getenv("TELEMETRY_SCENARIO", "console").split(",")
场景 用途 导出目标
console 本地开发调试 stdout
application_insights 生产监控 Azure Monitor
aspire_dashboard 本地可视化仪表盘 OTLP gRPC

SK 专属过滤器

python 复制代码
handler.addFilter(logging.Filter("semantic_kernel"))

只将 semantic_kernel.* 命名空间的日志接入 OpenTelemetry,避免全量日志污染 trace,精准追踪 SK 框架内的每一次工具调用和 LLM 推理。


大白话费曼解释 (Feynman Explanation)

类比一:多 Agent 意图切换 = 医院分诊台 + 专科诊室

想象你去医院看病。一进门先到分诊台detect_intent),护士简单问几句话,判断你是看内科还是外科。

然后你被带到内科诊室 (hotel_agent),里面的医生(Anna)专门负责内科问题,有自己的病历系统(Hotel_Tools)可以查你的住院记录、政策手册。

突然你说:"医生,我的腿也有点问题"------分诊台护士立刻出现,把你的病历(对话历史)拿走,送到隔壁外科诊室(flight_agent),里面的医生(Maya)接手,直接看你之前的病历继续诊治。

全程你没有离开医院、没有重新挂号、病历也没有丢失------这就是"无感 Agent 切换"的本质。

关键点 :分诊台护士(意图分类器)是独立于诊室医生(主模型)的人,她只需要问 2 句话就能判断,不需要和你聊 10 分钟------这就是为什么用轻量小模型而不是用主模型来做路由的原因。


类比二:RAG 防幻觉 = 餐厅服务员的严格规则

餐厅服务员(AI Agent)被老板(Persona YAML)严格培训:
"回答客人问题前,必须先去查菜单(调用 search_knowledgebase 工具),不准凭空编答案!"

客人问:"你们有素食选项吗?"

服务员不能拍脑袋说"有!有松茸炒饭!"------必须先去厨房拿今天的菜单(向量检索政策文档),把菜单上的内容读给客人听,如果菜单没写,就说"抱歉我不知道"。

向量检索就像菜单索引系统------你用语义"素食"去搜,它把最相关的菜品(top-3 政策条款)翻出来给你。服务员只能 cite 菜单上的内容,不能创作。

这就是 RAG 防幻觉的本质:把 LLM 的"创作自由"限制在工具返回的事实边界内。


降维打击与实战启发

Trick 1:轻量分类器解耦路由逻辑(可直接复用)

这是最值得"抄作业"的模式。任何多 Agent 系统都可以用这个模式:

python 复制代码
# ==================== 可复用的意图路由模板 ====================
# 适用于任何需要在多个专业 Agent 之间路由的场景

AGENT_DESCRIPTIONS = {
    "support_agent": "Handle technical support and bug reports",
    "sales_agent": "Handle pricing, licensing, and purchase inquiries",
    "billing_agent": "Handle invoices, refunds, and payment issues",
}

async def detect_intent_lightweight(conversation: str, current_agent: str) -> str:
    """
    用 GPT-4o-mini + max_tokens=20 做极轻量的意图路由。
    
    核心设计原则:
    1. max_tokens=20:强制模型只输出 agent 名称,不废话
    2. temperature 不设(默认1.0):分类任务反而受益于一点随机性(边界样本不会过拟合到某个固定答案)
    3. 系统提示明确枚举所有可能输出,防止幻觉出不在列表中的 agent 名
    """
    agent_list = "\n".join([f"- **{name}**: {desc}" 
                             for name, desc in AGENT_DESCRIPTIONS.items()])
    
    messages = [
        {
            "role": "system", 
            "content": f"""You are an intent classifier. 
Based on the conversation, output ONLY the name of the most appropriate agent.
Possible agents:
{agent_list}

Output rules:
- Output ONLY the agent name, nothing else
- If the current topic matches the current agent ({current_agent}), output: {current_agent}
- Never output anything not in the list above"""
        },
        {"role": "user", "content": conversation}
    ]
    
    response = await async_client.chat.completions.create(
        model="gpt-4o-mini",  # 关键:用小模型
        messages=messages,
        max_tokens=20          # 关键:强制短输出
    )
    
    intent = response.choices[0].message.content.strip()
    
    # 防御性校验:如果模型输出了不在列表中的内容,保持当前 agent
    return intent if intent in AGENT_DESCRIPTIONS else current_agent

Trick 2:LLM-as-Data-Generator + 自动质量过滤(意图分类器训练数据生成)

python 复制代码
# ==================== 合成训练数据生成模板 ====================
# 核心思路:用强模型(GPT-4)生成数据,用规则过滤确保质量

import json
from openai import AzureOpenAI
from typing import List, Dict

VALID_LABELS = {"hotel_agent", "flight_agent", "car_rental_agent", "general_agent"}

def build_generation_prompt(target_label: str, n_samples: int = 10) -> str:
    """
    关键技巧:在 prompt 中明确指定需要生成的类别,解决类别不平衡。
    """
    return f"""Generate {n_samples} customer service conversation examples 
where the final intent is: {target_label}

Each example must be JSON with keys: conversation_transcript, current_domain, intent_shift
Rules:
- conversation_transcript: list of 3-5 turns (agent/user alternating)
- current_domain: the domain being handled at start (can be different from intent_shift)
- intent_shift: "{target_label}" or "no_change" (if current_domain stays same)
- Include partial/incomplete user sentences to simulate real speech transcription

Output a JSON object with key "training_data" containing the list.
"""

def generate_and_filter(
    client: AzureOpenAI,
    model: str,
    target_label: str,
    n_batches: int = 10
) -> List[Dict]:
    """
    批量生成 + 四重质量过滤:
    1. JSON 格式合法性
    2. 必需字段完整性  
    3. intent_shift 标签合法性
    4. current_domain 合法性
    """
    results = []
    
    for _ in range(n_batches):
        response = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": build_generation_prompt(target_label)}],
            temperature=0.9,              # 高 temperature 保证多样性
            response_format={"type": "json_object"},  # 强制 JSON 输出,避免格式错误
        )
        
        try:
            data = json.loads(response.choices[0].message.content)
            for item in data.get("training_data", []):
                # 四重过滤
                if (
                    "conversation_transcript" in item and
                    "current_domain" in item and
                    "intent_shift" in item and
                    item["current_domain"] in VALID_LABELS and
                    (item["intent_shift"] in VALID_LABELS or item["intent_shift"] == "no_change")
                ):
                    results.append(item)
        except (json.JSONDecodeError, KeyError) as e:
            print(f"Skipping invalid batch: {e}")
            continue
    
    return results

def convert_to_classification_format(raw_data: List[Dict]) -> List[Dict]:
    """
    关键的 Label 转换:将 no_change → current_domain 名称
    使模型学会"当前该由哪个 agent 服务"而不是"是否发生了切换"
    这个 formulation 更鲁棒,适合 fine-tuning 多分类器
    """
    converted = []
    for item in raw_data:
        transcript = "\n".join(item["conversation_transcript"])
        intent = (item["intent_shift"] 
                  if item["intent_shift"] != "no_change" 
                  else item["current_domain"])  # ← 核心转换逻辑
        converted.append({
            "conversation": transcript,
            "intent": intent
        })
    return converted

Trick 3:双协程无阻塞 WebSocket 桥接模式

这个模式可复用于任何需要**"实时双向流量转发"**的场景(如 WebSocket proxy、音视频转码桥等):

python 复制代码
# ==================== 可复用的双协程 WebSocket 桥接模板 ====================
# 适用于:任何需要在两个 WebSocket 之间做实时双向转发的场景
# 优点:asyncio.gather 让两个方向完全并发,互不阻塞

import asyncio
import json
from aiohttp import web

async def bidirectional_ws_bridge(
    client_ws: web.WebSocketResponse,    # 来自用户的 WebSocket
    upstream_ws,                          # 连接到上游服务的 WebSocket
    transform_client_to_upstream=None,    # 可选:消息转换函数(client → upstream)
    transform_upstream_to_client=None,    # 可选:消息转换函数(upstream → client)
    on_upstream_event=None                # 可选:处理特殊上游事件的回调
):
    """
    核心设计:两个独立的 async 协程并发运行,
    通过 asyncio.gather 等待两者都完成(任一出错则全部取消)。
    
    这比"轮询"模式延迟更低,比"线程"模式资源占用更少。
    """
    
    async def forward_client_to_upstream():
        """方向1:客户端 → 上游,带可选转换"""
        async for msg in client_ws:
            if msg.type == web.WSMsgType.TEXT:
                data = json.loads(msg.data)
                if transform_client_to_upstream:
                    data = transform_client_to_upstream(data)
                if data:  # 转换后可能返回 None 表示过滤掉该消息
                    await upstream_ws.send_json(data)
            elif msg.type in (web.WSMsgType.CLOSE, web.WSMsgType.ERROR):
                break
    
    async def forward_upstream_to_client():
        """方向2:上游 → 客户端,带可选转换和特殊事件处理"""
        async for event in upstream_ws:
            if on_upstream_event:
                # 允许外部处理特殊事件(如记录历史、触发意图检测)
                should_forward = await on_upstream_event(event, client_ws)
                if not should_forward:
                    continue
            
            if transform_upstream_to_client:
                event = transform_upstream_to_client(event)
            
            if event:
                await client_ws.send_json(event)
    
    # 关键:gather 确保两个方向同时运行,任一退出则另一也退出
    await asyncio.gather(
        forward_client_to_upstream(),
        forward_upstream_to_client()
    )

架构全链路流程图 (Mermaid)

|"PCM16 base64 frames\ninput_audio_buffer.append"| WS_ENDPOINT

复制代码
PHONE --> ACS_WS
ACS_WS -->|"AudioData → input_audio_buffer.append\ncallerId as session_key"| WS_ENDPOINT

WS_ENDPOINT --> SESSION_STORE
SESSION_STORE -->|"transcription.completed\n触发意图检测"| DETECT

DETECT -->|"intent == flight_agent?"| FLIGHT_AGENT
DETECT -->|"intent == hotel_agent?"| HOTEL_AGENT

HOTEL_AGENT -->|"update_session\n+ Hotel_Tools Kernel"| AOAI_RT
FLIGHT_AGENT -->|"update_session\n+ Flight_Tools Kernel"| AOAI_RT

AOAI_RT -->|"Function Calling"| HOTEL_TOOLS
AOAI_RT -->|"Function Calling"| FLIGHT_TOOLS
AOAI_RT -->|"response.audio.delta\nPCM16 base64 frames"| WS_ENDPOINT

HOTEL_TOOLS --> HOTEL_DB
HOTEL_TOOLS --> HOTEL_VEC
FLIGHT_TOOLS --> FLIGHT_DB
FLIGHT_TOOLS --> FLIGHT_VEC

WS_ENDPOINT -->|"history.reduce()\npickle+base64"| REDIS
REDIS -->|"session resume\nrestore history"| SESSION_STORE

WS_ENDPOINT -->|"audio.delta frames"| WEB
WS_ENDPOINT -->|"response.audio.delta\n→ ACS AudioData"| ACS_WS
ACS_WS -->|"PCM24K audio\n+ StopAudio barge-in"| PHONE

Backend --> OTEL
OTEL --> APPINS
OTEL --> ASPIRE

style DETECT fill:#ff9,stroke:#f90,color:#000
style AOAI_RT fill:#9cf,stroke:#06f,color:#000
style REDIS fill:#f9f,stroke:#c0c,color:#000
style HOTEL_VEC fill:#cfc,stroke:#090,color:#000
style FLIGHT_VEC fill:#cfc,stroke:#090,color:#000 -->

总结:这个方案的真正价值

维度 本方案的水准
架构可扩展性 ⭐⭐⭐⭐⭐ YAML 驱动,新增 Agent 零改代码
防幻觉可靠性 ⭐⭐⭐⭐ 三道防线(Prompt约束 + 工具接地 + 历史截断),但**无 Citation 溯源**
实时延迟 ⭐⭐⭐⭐⭐ Server VAD + GPT-4o Realtime,<200ms token 延迟
工程成熟度 ⭐⭐⭐⭐⭐ OpenTelemetry全链路 + Redis分布式状态 + 容器化部署
意图路由精度 ⭐⭐⭐⭐ Fine-tuned + GPT-4o-mini双模式,3253条合成数据训练
知识检索深度 ⭐⭐⭐ 纯向量检索,无BM25混合/Reranker,适合小规模KB

最核心的启发 :这个项目展示了一个生产级 AI 系统的"正确打开方式"------不是堆砌最复杂的技术,而是在正确的地方用正确的技术:路由用小模型、生成用大模型、状态用 Redis、监控用 OpenTelemetry。架构的简洁性本身就是最高级的工程智慧。

参考

https://github.com/microsoft/multi-modal-customer-service-agent

好的,我来做一个从零开始、完整串联的详细讲解,确保小白也能一步步看懂整条链路。


精确SQL查询之load_user_reservation_info 完整调用链路详解

先用一个生活类比建立整体感知

想象你打电话给酒店客服:

你说:"我想查一下我的预订信息"

背后发生的事:

  1. 电话接线员(AI Agent "Anna")听到你说话 → 语音变成文字
  2. Anna 意识到需要查数据库 → 决定调用一个查询工具
  3. 查询工具拿着你的客户ID → 去数据库翻档案
  4. 数据库返回预订记录 → 工具把结果交回给 Anna
  5. Anna 用自然语言告诉你预订详情 → 语音回复给你

load_user_reservation_info 就是上面第 3 步那个****"去数据库翻档案"****的工具。


数据库长什么样(地基)

首先要理解这个工具查的是什么数据库。数据库文件是 hotel.db(SQLite 格式),里面有两张表:

plain 复制代码
【customers 表】------ 客户信息
┌──────────┬──────────┐
│    id    │   name   │
├──────────┼──────────┤
│  12345   │ John Doe │   ← user_profile.json 里定义的测试用户
└──────────┴──────────┘

【reservations 表】------ 预订信息
┌─────┬─────────────┬──────────┬───────────┬──────────────┬───────────────┬────────┐
│ id  │ customer_id │ hotel_id │ room_type │ check_in_date│check_out_date │ status │
├─────┼─────────────┼──────────┼───────────┼──────────────┼───────────────┼────────┤
│ 001 │    12345    │  HTL_001 │  Deluxe   │  2024-03-01  │  2024-03-05   │ booked │
│ 002 │    12345    │  HTL_002 │  Suite    │  2024-04-10  │  2024-04-12   │ booked │
└─────┴─────────────┴──────────┴───────────┴──────────────┴───────────────┴────────┘

这个函数的完整代码,逐行拆解

python 复制代码
# hotel_plugins.py 第 180-200 行

@kernel_function(                                      # ← 第1层:装饰器,让 Semantic Kernel 认识这个函数
    name="load_user_reservation_info",                 # ← SK 注册的工具名(GPT-4o 调用时用这个名字)
    description="Loads the hotel reservation for a user."  # ← GPT-4o 靠这句描述判断要不要调用它
)
async def load_user_reservation_info(
    self,
    user_id: Annotated[str, "The user id."]            # ← 第2层:参数注解,告诉 GPT-4o 需要传什么
) -> str:                                              # ← 返回值是字符串(JSON 格式的字符串)

    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    # 第3层:实际的数据库查询
    # 等价的 SQL 语句是:
    # SELECT * FROM reservations
    # WHERE customer_id = '12345'
    # AND status = 'booked'
    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    reservations = session.query(Reservation).filter_by(
        customer_id=user_id,   # ← 条件1:必须是这个用户的
        status="booked"        # ← 条件2:只查"已预订"状态,不查已取消的
    ).all()                    # ← .all() = 取出所有符合条件的记录(可能有多条)

    # 如果什么都没查到,直接返回一句话
    if not reservations:
        return "Sorry, we cannot find any reservation information for you."

    # 把查出来的数据库对象列表,转成 JSON 字符串返回给 GPT-4o
    return json.dumps([
        {
            'room_type':      reservation.room_type,
            'hotel_id':       reservation.hotel_id,
            'check_in_date':  reservation.check_in_date.strftime('%Y-%m-%d'),  # 日期格式化
            'check_out_date': reservation.check_out_date.strftime('%Y-%m-%d'),
            'reservation_id': reservation.id,
            'status':         reservation.status
        }
        for reservation in reservations   # ← 列表推导式,把每条记录都转成字典
    ])

四、完整的端到端调用流程(9 步图解)

plain 复制代码
用户语音:"帮我查一下我的酒店预订"
        │
        ▼ Step 1: GPT-4o Realtime API 把语音转文字(Whisper)
"帮我查一下我的酒店预订"
        │
        ▼ Step 2: rtmt.py 收到转录完成事件,把文字加入对话历史
session["history"].add_user_message("帮我查一下我的酒店预订")
        │
        ▼ Step 3: rtmt.py 触发 response.create,让 GPT-4o 思考如何回复
        │
        ▼ Step 4: GPT-4o 读取对话历史 + Persona 指令,决定调用工具
        │
        │   GPT-4o 的内部推理(Function Calling):
        │   "用户要查预订 → 我有 load_user_reservation_info 工具
        │    它需要 user_id → 从 session 上下文知道是 '12345'
        │    → 发出工具调用请求"
        │
        ▼ Step 5: Semantic Kernel 拦截 Function Call,执行 Python 函数
hotel_tools.load_user_reservation_info(user_id="12345")
        │
        ▼ Step 6: SQLAlchemy 执行数据库查询
SELECT * FROM reservations WHERE customer_id='12345' AND status='booked'
        │
        ▼ Step 7: 数据库返回结果,Python 函数序列化成 JSON 字符串
'[{"room_type": "Deluxe", "hotel_id": "HTL_001", 
   "check_in_date": "2024-03-01", "check_out_date": "2024-03-05",
   "reservation_id": "001", "status": "booked"}]'
        │
        ▼ Step 8: SK 把这个 JSON 字符串作为工具结果,返回给 GPT-4o
        │
        ▼ Step 9: GPT-4o 读取工具结果,用自然语言生成最终回复
"您好 John!我查到您有一条预订记录:
 入住豪华间,酒店编号 HTL_001,
 入住日期 3月1日,退房日期 3月5日,
 预订状态:已确认。请问还有什么需要帮助的吗?"
        │
        ▼ 语音合成后播放给用户

customer_id 是怎么传进来的?(关键细节)

GPT-4o 怎么知道要传 user_id="12345"

答案在 ****Persona 模板里

yaml 复制代码
# hotel_agent_profile.yaml
persona: |
  You are Anna, a hotel customer service agent.
  You are currently serving {customer_name}, whose ID is {customer_id}.
  # ↑↑↑ 这里的 {customer_id} 在每次对话建立时被替换成真实值

rtmt.py 里,用户通过 URL 传入客户信息:

python 复制代码
# rtmt.py 第 425-426 行
# 用户连接 WebSocket 时附带的参数
customer_name = request.query.get("customer_name", "John Doe")
customer_id   = request.query.get("customer_id", "12345")

然后注入到 Persona

python 复制代码
# rtmt.py 第 220-226 行
def _format_instructions(self, agent: dict, session: dict) -> str:
    template = agent.get("persona", "")
    return template.format(
        customer_name=session.get("customer_name", "John Doe"),
        customer_id=session.get("customer_id", "12345")
        # ↑ 把占位符替换成真实值,写入 GPT-4o 的 System Prompt
    )

结果:GPT-4o 的 System Prompt 里有这么一句:

plain 复制代码
"You are currently serving John Doe, whose ID is 12345."

所以当 GPT-4o 决定调用 load_user_reservation_info 时,它直接从 System Prompt 里知道 user_id = "12345",无需用户再说一遍。


所有酒店工具函数一览对比(完整版)

函数名 触发场景(用户说什么) 需要的参数 实际执行的操作 返回结果示例
load_user_reservation_info "查我的预订" / 对话开始时主动问候 user_id SELECT * FROM reservations WHERE customer_id=? AND status='booked' 所有预订的 JSON 数组
check_reservation_status "我的预订状态怎么样" + 指定预订号 reservation_id SELECT * FROM reservations WHERE id=? AND status='booked' 单条预订的 JSON
check_change_reservation "我想改一下入住日期,要多少钱?" reservation_id + 新日期 + 新房型 不查数据库,直接返回固定费用 "Changing will cost $50"
confirm_reservation_change "好的,我确认改订" reservation_id + 新日期 + 新房型 UPDATE 旧记录为 cancelled + INSERT 新记录 新预订确认信息
query_rooms "还有哪些房型可以选?" hotel_id + 入退住日期 不查数据库,返回固定的三种房型 标准间/豪华间/套房列表
search_hotel_knowledgebase "宠物政策是什么?" / "几点可以入住?" search_query(问题文本) 向量检索政策文档(完全不碰 SQL) Top-3 政策条款文本

可以直接运行的完整独立示例代码

下面这段代码把整个工具调用链路从零独立复现,不依赖 Semantic Kernel,你可以直接在本地运行理解:

python 复制代码
"""
完整独立示例:模拟 load_user_reservation_info 的完整工作流
依赖:pip install sqlalchemy
"""

import json
from datetime import datetime
from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship

# ═══════════════════════════════════════════════════
# 第一步:定义数据库结构(和项目代码完全一致)
# ═══════════════════════════════════════════════════
Base = declarative_base()

class Customer(Base):
    __tablename__ = 'customers'
    id = Column(String, primary_key=True)
    name = Column(String)
    reservations = relationship('Reservation', backref='customer')

class Reservation(Base):
    __tablename__ = 'reservations'
    id = Column(Integer, primary_key=True, autoincrement=True)
    customer_id = Column(String, ForeignKey('customers.id'))
    hotel_id = Column(String)
    room_type = Column(String)
    check_in_date = Column(DateTime)
    check_out_date = Column(DateTime)
    status = Column(String)

# ═══════════════════════════════════════════════════
# 第二步:创建内存数据库并插入测试数据
# ═══════════════════════════════════════════════════
engine = create_engine('sqlite:///:memory:')   # 内存数据库,运行完自动消失
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
db = Session()

# 插入一个测试客户
db.add(Customer(id="12345", name="John Doe"))

# 插入两条预订记录
db.add(Reservation(
    customer_id="12345",
    hotel_id="HTL_001",
    room_type="Deluxe",
    check_in_date=datetime(2024, 3, 1),
    check_out_date=datetime(2024, 3, 5),
    status="booked"
))
db.add(Reservation(
    customer_id="12345",
    hotel_id="HTL_002",
    room_type="Suite",
    check_in_date=datetime(2024, 4, 10),
    check_out_date=datetime(2024, 4, 12),
    status="cancelled"       # ← 注意:这条是 cancelled,不会被查出来
))
db.commit()

# ═══════════════════════════════════════════════════
# 第三步:完整复现 load_user_reservation_info 函数
# ═══════════════════════════════════════════════════
def load_user_reservation_info(user_id: str) -> str:
    """
    完整复现项目中的工具函数
    """
    print(f"\n📌 [工具被调用] load_user_reservation_info(user_id='{user_id}')")
    print(f"   等价 SQL: SELECT * FROM reservations WHERE customer_id='{user_id}' AND status='booked'")

    # 执行查询(只查 status='booked' 的记录)
    reservations = db.query(Reservation).filter_by(
        customer_id=user_id,
        status="booked"
    ).all()

    print(f"   查询结果:找到 {len(reservations)} 条记录")

    if not reservations:
        return "Sorry, we cannot find any reservation information for you."

    result = json.dumps([
        {
            'room_type':      r.room_type,
            'hotel_id':       r.hotel_id,
            'check_in_date':  r.check_in_date.strftime('%Y-%m-%d'),
            'check_out_date': r.check_out_date.strftime('%Y-%m-%d'),
            'reservation_id': r.id,
            'status':         r.status
        }
        for r in reservations
    ], indent=2, ensure_ascii=False)

    return result


# ═══════════════════════════════════════════════════
# 第四步:模拟 GPT-4o Function Calling 的完整决策过程
# ═══════════════════════════════════════════════════
def simulate_gpt4o_function_calling(user_message: str, customer_id: str):
    """
    模拟 GPT-4o 收到用户消息后,决定调用哪个工具的过程
    (真实场景下这一步由 GPT-4o 自动完成)
    """
    print(f"\n{'='*60}")
    print(f"👤 用户说:{user_message}")
    print(f"{'='*60}")

    # GPT-4o 根据 description 判断要调用哪个工具
    # "Loads the hotel reservation for a user." 
    # → 用户问的是预订信息 → 调用这个工具
    print("\n🤖 GPT-4o 决策:")
    print("   → 用户想查预订信息")
    print("   → 我有 load_user_reservation_info 工具(描述:Loads the hotel reservation for a user)")
    print(f"   → System Prompt 告诉我当前用户 ID 是 '{customer_id}'")
    print(f"   → 发出 Function Call: load_user_reservation_info(user_id='{customer_id}')")

    # 实际执行工具
    tool_result = load_user_reservation_info(customer_id)

    print(f"\n📦 [工具返回结果]:")
    print(tool_result)

    # GPT-4o 读取工具结果,生成自然语言回复
    parsed = json.loads(tool_result)
    r = parsed[0]
    gpt_response = (
        f"您好!我查到您有 {len(parsed)} 条有效预订:\n"
        f"  入住 {r['room_type']} 房型,酒店编号 {r['hotel_id']}\n"
        f"  入住日期:{r['check_in_date']},退房日期:{r['check_out_date']}\n"
        f"  预订编号:{r['reservation_id']},状态:{r['status']}\n"
        f"请问还有什么需要帮助的吗?"
    )

    print(f"\n💬 [GPT-4o 最终回复]:")
    print(gpt_response)
    return gpt_response


# ═══════════════════════════════════════════════════
# 运行示例
# ═══════════════════════════════════════════════════
if __name__ == "__main__":

    # 场景1:正常查询(有预订记录)
    simulate_gpt4o_function_calling(
        user_message="帮我查一下我的酒店预订",
        customer_id="12345"
    )

    # 场景2:查询不存在的用户
    simulate_gpt4o_function_calling(
        user_message="查一下我的预订",
        customer_id="99999"  # 数据库里没有这个用户
    )

运行输出

plain 复制代码
============================================================
👤 用户说:帮我查一下我的酒店预订
============================================================

🤖 GPT-4o 决策:
   → 用户想查预订信息
   → 我有 load_user_reservation_info 工具(描述:Loads the hotel reservation for a user)
   → System Prompt 告诉我当前用户 ID 是 '12345'
   → 发出 Function Call: load_user_reservation_info(user_id='12345')

📌 [工具被调用] load_user_reservation_info(user_id='12345')
   等价 SQL: SELECT * FROM reservations WHERE customer_id='12345' AND status='booked'
   查询结果:找到 1 条记录       ← cancelled 那条被过滤了

📦 [工具返回结果]:
[
  {
    "room_type": "Deluxe",
    "hotel_id": "HTL_001",
    "check_in_date": "2024-03-01",
    "check_out_date": "2024-03-05",
    "reservation_id": 1,
    "status": "booked"
  }
]

💬 [GPT-4o 最终回复]:
您好!我查到您有 1 条有效预订:
  入住 Deluxe 房型,酒店编号 HTL_001
  入住日期:2024-03-01,退房日期:2024-03-05
  预订编号:1,状态:booked
请问还有什么需要帮助的吗?

============================================================
👤 用户说:查一下我的预订
============================================================
...
📦 [工具返回结果]:
"Sorry, we cannot find any reservation information for you."

三个最容易踩坑的细节

坑1: status="booked"** 过滤条件**

python 复制代码
# ✅ 项目代码:只查 booked 状态
.filter_by(customer_id=user_id, status="booked")

# 如果不加 status 过滤:
# 用户改了订单 → 旧订单变成 cancelled → 查询会把 cancelled 的也返回
# 导致 AI 告诉用户"您有2条预订",其中一条其实已经取消了!

坑2: session** 是模块级单例(线程安全隐患)**

python 复制代码
# hotel_plugins.py 第 28 行
session = Session()  # ← 模块加载时创建,全局共享

# ⚠️ 风险:多个用户并发时共用同一个 session
# 项目的规避方式:SQLite 本身是文件锁,对于演示场景够用
# 生产环境建议:每次请求创建新 session(用 with Session() as s:)

坑3: Annotated** 注解不是普通类型提示**

python 复制代码
user_id: Annotated[str, "The user id."]
#                   ↑           ↑
#                 实际类型    给 GPT-4o 看的参数说明

# Semantic Kernel 会把这个说明提取出来,
# 组装成 OpenAI Function Calling 的 JSON Schema,
# 让 GPT-4o 知道这个参数"是什么、填什么"

生成的 Function Schema 大概长这样(SK 自动生成,你不用手写):

json 复制代码
{
  "name": "load_user_reservation_info",
  "description": "Loads the hotel reservation for a user.",
  "parameters": {
    "type": "object",
    "properties": {
      "user_id": {
        "type": "string",
        "description": "The user id."
      }
    },
    "required": ["user_id"]
  }
}

这就是 GPT-4o "看到工具说明书"的原始格式。<font style="color:#DF2A3F;">@kernel_function</font> 装饰器 + <font style="color:#DF2A3F;">Annotated</font> 注解共同完成了这份"说明书"的自动生成,只需要写 Python 函数,剩下的 SK 搞定

相关推荐
fqrj20264 小时前
公司网站设计制作费用详解:影响价格的关键因素
microsoft·.net·网站建设
Elastic 中国社区官方博客4 小时前
将 Logstash 管道从 Azure Event Hubs 迁移到 Kafka 输入插件
大数据·数据库·elasticsearch·microsoft·搜索引擎·kafka·azure
喵叔哟4 小时前
5.【.NET10 实战--孢子记账--产品智能化】--基础框架与微软官方包批量升级
人工智能·microsoft·.net
葡萄城技术团队4 小时前
Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度
android·java·microsoft
东方隐侠安全团队-千里5 小时前
基于SAST+AI代码审计 架构与功能详解
人工智能·microsoft·架构
AI品信智慧数智人5 小时前
数字人智能交互系统集成大屏展示:解锁可视化交互新境界✨
microsoft
DYuW5gBmH1 天前
Anthropic 开源 Bloom:基于 LLM 的自动化行为评估框架
运维·microsoft·自动化
小龙报1 天前
【Coze-AI智能体平台】Coze OpenAPI 开发手册:鉴权、接口调用与 SDK 实践
javascript·人工智能·python·深度学习·microsoft·文心一言·开源软件
weitingfu1 天前
Excel VBA 入门到精通(二):变量、数据类型与运算符
java·大数据·开发语言·学习·microsoft·excel·vba