首页 / 文章

Azure上用于执行摘要生成代理的四层架构

一个确定性报告生成工具如何将任务分配到Fabric、SQL存储过程、Azure Functions上的FastAPI以及Copilot Studio,从而生成执行报告。

发布者:REACTAPP.TOP阅读约 22 分钟5073 词

来源: **Chief of Staff Executive Briefing Agent: Enterprise-scale layered architecture for automated intelligence generation**

任何为高管准备客户会议的人都知道相关资料是多么零散。CRM记录存放在一个系统中,营收数据在另一个系统里,产品使用情况又在第三个系统中,而完整的战略汇报则需要手动将这些分散的信息整合起来。所谓“幕僚长”简报工具正是为了解决这一难题:用户用通俗的语言提出需求,该平台便会根据零散的运营、财务以及人工智能应用数据生成一份可供高管直接使用的文档。

这项设计值得研究之处在于它几乎不依赖偶然因素。它并非让模型根据提示来撰写说明,而是用受合同约束的协调机制取代手动汇总的方式。数据在治理框架下被整合,通过确定的SQL语句进行汇总,经由无状态服务处理,并通过受控的对话流程传递。该架构在基础层、数据层、服务层和体验层之间划定了明确的执行边界,从而能够在严格的安保要求下实现结果的复现、审计,并以企业级的速度交付。

该设计源自微软生态系统内的内部人工智能孵化项目,下文所述的性能与效果声明均为开发方自述,并非通过独立测试得出。它最适合那些需要能够实现数据整合、SQL聚合、无状态编排以及对话式工作流分离的确定性人工智能系统的企业架构师、人工智能工程师及平台负责人。

四层设计

该代理遵循四层编排模型,每一层都有特定的功能,并涉及多个Azure服务及安全边界:

  • 基础层:微软Fabric Lakehouse,用于在受控环境下整合数据。
  • 数据层:Fabric SQL(仓库或SQL端点),存储用于操作查询和聚合的存储过程。
  • 服务:部署在 Azure Functions 中的 FastAPI 编排引擎。
  • 体验:能够理解用户意图的 Copilot Studio 对话式工作流。
  • 二者共同构成一个单向智能处理流程。数据在底层经过筛选与管理,通过 SQL 以确定性方式聚合,在服务层由领域逻辑进行整合,最终通过自然语言交互呈现出来。每一层都与相邻层保持明确的接口规范,因此人工智能元素能够辅助决策而非干扰决策过程。

    基础层:受控的数据整合

    Microsoft Fabric 充当统一的分析平台,同时也是账户详情、财务数据、使用指标及历史趋势的单一真实数据源。其核心原则很简单:仅存储一次数据,多次重复使用,从而避免系统间出现数据重复。

    该管道每天都会刷新,从 Dynamics 365、财务系统及外部 API 中获取数据并合并。这样的调度方式能够确保有规律的处理时间,并保证在开始整合之前所有源系统都已完成各自的日常任务。Lakehouse 以原始格式保存数据,同时保留数据溯源信息及治理标准,经过处理后的数据集则会进入 Fabric SQL——这一专为生成管理层汇报而优化的快速操作查询层。

    数据层:专为 API 访问设计的操作引擎

    该层将经过筛选的基础数据转换为确定性、低延迟的查询契约,用于生成简报。它设定了严格的聚合边界,以及高管们期望从高效工具中获得的性能保障。

    架构。Fabric SQL Warehouse是一种关系型接口,它将所有与报告相关的数据操作都封装在存储过程中。服务层从不直接查询表数据。跨表的复杂连接、各项聚合操作的含义以及业务逻辑均在数据库内部执行,而非在应用程序代码中运行,这有助于在处理大量数据时保持响应时间的稳定性。该SQL语言针对操作型查询进行了优化,而非分析型探索;其设计目标是为确保结果一致性而实现确定性连接,通过可重用的查询计划维持稳定性能,利用参数化执行提升安全性,并采用更易于维护的封装式聚合方式。

    模式设计理念:以查询优先。该模式遵循查询模式而非传统的规范化原则。Account实体是主要的聚类结构,而辅助表则通过业务标识符TPID进行组织,从而实现高效的连接操作。

         --Accounts Table - Primary Business Entity
    
    CREATE TABLE [aieo].[Accounts] (
    
        [Id] INT IDENTITY(1,1) PRIMARY KEY,
    
        [TPID] BIGINT NOT NULL,
    
        [AccountName] NVARCHAR(255) NOT NULL,
    
        [AccountNumber] NVARCHAR(50),
    
        [SearchTokens] NVARCHAR(500),
    
        [Segment] NVARCHAR(100),
    
        [Industry] NVARCHAR(100),
    
        [ACR_FY25] DECIMAL(15,2),
    
        [RevenueRank] INT,
    
        [CreatedDate] DATETIME2 DEFAULT GETUTCDATE(),
    
        [LastSync] DATETIME2,
    
        [IsActive] BIT DEFAULT 1
    
    );
    
    
    
    CREATE CLUSTERED INDEX IX_Accounts_TPID ON [aieo].[Accounts]([TPID]);
    
    CREATE NONCLUSTERED INDEX IX_Accounts_Name ON [aieo].[Accounts](
    

    “账户”表是核心实体,以TPID作为其聚类键。这为最常见的账户相关查询提供了近似对数级的检索速度,同时支持直接通过TPID查询以及按名称搜索。

    辅助表。部分辅助表展示了财务指标、AI使用分析以及跨系统数据补充的处理方式。

    -- Revenue Table - Historical and Forecast Data
    
    CREATE TABLE [aieo].[Revenue] (
    
        [TPID] BIGINT,
    
        [FiscalYear] NVARCHAR(10),
    
        [RevenueType] NVARCHAR(20),
    
        [Amount] DECIMAL(15,2),
    
        [LastUpdated] DATETIME2
    
    );
    
    CREATE INDEX IX_Revenue_TPID_FY ON [aieo].[Revenue]([TPID], [FiscalYear]);
    

    收入表保存了数年的财务数据,并使用适合时间序列与趋势分析的复合索引。

    -- AI Usage Table - Product Adoption Metrics
    
    CREATE TABLE [aieo].[AIUsage] (
    
        [TPID] BIGINT,
    
        [ProductCategory] NVARCHAR(50),
    
        [UsageLevel] NVARCHAR(20),
    
        [SeatCount] INT,
    
        [AdoptionRate] DECIMAL(5,2),
    
        [LastUpdated] DATETIME2
    
    );
    
    CREATE INDEX IX_AIUsage_TPID_Product ON [aieo].[AIUsage]([TPID], [ProductCategory]);
    

    AI使用情况表按类别记录产品采用情况,有助于分析对账户规划至关重要的用户参与模式和技术应用状况。

    存储过程策略:业务逻辑存在于数据库中。三个核心过程负责生成所有智能分析结果。SearchAccounts用于查找账户并按收入对其进行排序,GetAccountDetails则将来自多个来源的数据整合起来,为管理层提供完整信息,而GetBriefingTemplate负责基于角色的模板授权机制,确保只有合适的人员能够访问相关内容。服务层仅调用这些过程,不允许编写动态SQL。

    --Account Search Procedure - Revenue-Prioritized Discovery
    
    CREATE OR ALTER PROCEDURE [aieo].[SearchAccounts]
    
        @searchTerm NVARCHAR(255),
    
        @top INT = 5
    
    AS
    
    BEGIN
    
        SET NOCOUNT ON;
    
    
    
        DECLARE @likeTerm NVARCHAR(257) = '%' + REPLACE(REPLACE(@searchTerm, '[', '[[]'), '%', '[%]') + '%'
    
    
    
        SELECT TOP (@top)
    
            a.TPID,
    
            a.AccountName,
    
            a.AccountNumber,
    
            ISNULL(acr.ACR_YTD, 0) as ACR_YTD
    
        FROM [aieo].[Accounts] a
    
        LEFT JOIN [aieo].[ACR] acr ON a.TPID = acr.TPID
    
        WHERE
    
            a.AccountName LIKE @likeTerm
    
            OR CAST(a.TPID AS NVARCHAR) = @searchTerm
    
            OR a.AccountNumber LIKE @likeTerm
    
        ORDER BY
    
            ISNULL(acr.ACR_YTD, 0) DESC,
    
            a.AccountName ASC
    
        FOR JSON PATH;
    
    END;
    
    --Account Details Retrieval Procedure - Comprehensive Intelligence Aggregation
    
    CREATE PROCEDURE [aieo].[GetAccountDetails]
    
    (@TPID int)
    
    AS
    
    BEGIN
    
        SET NOCOUNT ON;
    
        SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    
    
    
        SELECT TOP 1
    
            JSON_QUERY(ap.[AccountProfile]) AS [Account],
    
            JSON_QUERY(ai.[AIUsage]) AS [AI],
    
            JSON_QUERY(acr.[Data]) AS [ACR],
    
            deal.[Deals.AgreementValue],
    
            deal.[Deals.DealType],
    
            deal.[Deals.Term],
    
            deal.[Deals.Remaining],
    
            deal.[Deals.EndDate],
    
            ecif.[ECIF.Committed],
    
            aco.[ACO.IncrementalRevenue],
    
            aco.[ACO.Discount],
    
            aco.[ACO.ACO],
    
            JSON_QUERY(bot.[Data]) AS [BoT],
    
            JSON_QUERY(inv.[Data]) AS [Investments],
    
            JSON_QUERY(rev.[Data]) AS [Revenue]
    
        FROM [dbo].[vw_Customer] P
    
        OUTER APPLY (
    
            SELECT TOP 1
    
                a.[TPID],
    
                a.[CRMAccountName] AS [AccountName],
    
                a.[Segment],
    
                a.[Industry],
    
                a.[EOU],
    
                a.[OU],
    
                LOWER(TRIM(c.[Value])) AS [ATU_Manager.Alias],
    
                aau.[Mail] AS [ATU_Manager.Email],
    
                [dbo].[RemoveJobTitle](aau.[DisplayName]) AS [ATU_Manager.DisplayName],
    
                NULLIF(TRIM(aau.[BusinessPhone]), '') AS [ATU_Manager.PhoneNumber],
    
                JSON_QUERY([am].[Data]) AS [AM],
    
                JSON_QUERY([atum].[Data]) AS [ATUM]
    
            FROM [dbo].[vw_Customer] A
    
            OUTER APPLY STRING_SPLIT(a.[AM], ',', 1) C
    
            LEFT JOIN [cm].[vw_AAD_User] AAU ON LOWER(TRIM(C.[Value])) = LOWER(AAU.[UserPrincipalName])
    
            WHERE A.TPID = P.TPID
    
        ) ap
    
        WHERE P.TPID = @TPID;
    
    END;
    
    --Template Management Procedure - Role-Based Access Control
    
    CREATE OR ALTER PROCEDURE [aieo].[GetBriefingTemplate]
    
    (
    
        @UserAlias NVARCHAR(200),
    
        @BriefingType NVARCHAR(200)
    
    )
    
    AS
    
    BEGIN
    
        SET @UserAlias = IIF(CHARINDEX('@', @UserAlias) > 0,
    
                            LEFT(@UserAlias, CHARINDEX('@', @UserAlias) - 1),
    
                            @UserAlias)
    
    
    
        SELECT DISTINCT
    
            [BriefingTemplateId],[TemplateDescription],[BriefingType],
    
            [UserAlias],FullName,[Filename],[FileType],[PreviewFilename]
    
        FROM [hr].[DimPerson] p
    
        INNER JOIN [aieo].[ExecutiveOfficeMember] eom ON eom.[PersonnelNumber] = p.[PersonnelNumber]
    
        INNER JOIN [aieo].[ExecutiveOffice] o ON o.[ExecutiveOfficeId] = eom.[ExecutiveOfficeId]
    
        INNER JOIN [aieo].[BriefingTemplate] bt ON bt.[ExecutiveOfficeId] = o.[ExecutiveOfficeId]
    
    
    
        UNION
    
    
    
        SELECT DISTINCT
    
            [BriefingTemplateId],[TemplateDescription],[BriefingType],
    
            p.EmailName As UserAlias,p.FullName,[Filename],[FileType],[PreviewFilename]
    
        FROM [aieo].[BriefingTemplate] bt
    
        CROSS JOIN [hr].[DimPerson] p
    
        WHERE bt.[UserAlias] = 'ALL' AND p.EmailName = @UserAlias
    
            AND bt.BriefingType = @BriefingType;
    
    END;
    

    性能与安全性。为提升效率,事务会使用SET NOCOUNT ON;为避免读取操作造成阻塞,还会设置SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED。JSON处理依赖SQL Server内置的JSON_QUERY和FOR JSON PATH功能,这些功能能够高效处理数据并直接从数据库进行序列化。仅允许使用参数化执行方式,应用程序的服务身份仅拥有存储过程的执行权限,而无权直接访问表数据。

    GRANT EXECUTE ON SCHEMA::[aieo] TO [service_identity];
     - No direct table access permitted
    

    服务层:无状态编排与执行边界

    服务层仅负责纯粹的数据编排工作。它负责获取数据、渲染模板,而不应用任何自身的业务规则。每个服务都有明确的职责且彼此松耦合,从而使数据层与展示层保持清晰分离。

    API接口与验证。 FastAPI应用会暴露明确的REST端点,并通过Pydantic模型定义强类型契约:

    • POST /api/briefing/generate是主要的报告生成端点,具备完整的输入验证功能。
    • GET /api/health用于报告系统状态。
    • GET /api/templates会列出调用用户可使用的模板列表。
    from pydantic import BaseModel
    from typing import Optional
    class BriefingRequest(BaseModel):
    account_name_or_tpid: str
    meeting_datetime: Optional[str] = None
    meeting_objective: Optional[str] = None
    ms_attendees: Optional[str] = None
    template_type: Optional[str] = None
    

    该模型实施严格的验证与类型安全检查,自动生成 OpenAPI 文档,并规范每个请求的结构。任何非结构化输入都无法通过这一验证。

    执行模型。该服务会按固定的五步流程运行:

    1. 请求验证。Pydantic 模型确保类型和输入符合要求。
    2. 账户解析。根据输入格式决定是直接查找 TPID,还是通过 SearchAccounts 按名称进行搜索。
    3. 并发获取。通过 asyncio.gather() 同时执行 GetAccountDetails 和 GetBriefingTemplate。
    4. 上下文整合。处理结果直接映射为可用于模板的对象,无需中间处理。
  • 模板渲染。Jinja2能够以确定的格式生成执行文档。
  • 通过DatabaseService抽象实现异步执行和连接池技术,确保相同的输入总是产生相同的输出。

    指导原则。服务层遵循五项规则。其中不包含任何业务逻辑,因为智能处理仅在存储过程内部完成,这样服务层便无需遵循那些会增加测试和维护复杂度的规则。它仅负责纯粹的数据协调工作,负责数据的获取与呈现,而不进行任何转换或计算。上下文映射是直接的,因此过程结果会直接进入模板结构,不会经过任何中间环节,从而降低了数据与呈现内容之间含义偏差的风险。处理过程是确定性的,无论何时何地或基础设施状态如何,相同的输入都会产生相同的输出。此外,各功能模块被清晰地分开:数据逻辑存在于SQL过程中,协调功能在Python FastAPI服务中实现,而呈现功能则由Jinja2模板负责。

    体验层:Copilot Studio作为对话引擎

    在顶层,Microsoft Copilot Studio中的AdaptiveDialog框架可将自然语言请求转化为确定的流程。它负责处理账户解析、模板授权、外部数据集成,以及通过多种渠道生成和交付文档。整个流程从首次请求开始,依次进行账户解析、模板授权、外部数据收集并传输到SharePoint,在每条对话路径上均设有决策点与验证步骤。

    核心对话功能。该对话功能会自动收集账户标识符,确定启动流程的触发语句,并设定对话的主要入口点:

    kind: AdaptiveDialog
    
    inputs:
    
      - kind: AutomaticTaskInput
    
        propertyName: Account_Name_or_TPID
    
        name: Account Name or TPID
    
        description: The account/customer TPID (unique identifier) or name
    
        shouldPromptUser: true
    
    
    
    modelDescription: Create briefing document to prepare for meeting with customer
    
    beginDialog:
    
      kind: OnRecognizedIntent
    
      intent:
    
        displayName: Topic for creating briefing document to prepare for meeting with customer
    
        triggerQueries:
    
          - Generate a briefing document for account Accenture
    
          - I need a briefing for Amazon
    
          - Create briefing for Apple
    
          - I have a meeting with JPMorgan Chase at 4PM
    

    账户解析。通过IsNumeric()检查可将输入分为直接TPID或需要查询的账户名称。尽可能跳过查询可以减少API调用次数,从而加快响应速度:

    - kind: SetVariable
    
      id: setVariable_IsNumeric
    
      variable: Topic.IsNumeric
    
      value: =IsNumeric(Topic.UserInput)
    
    
    
    - kind: ConditionGroup
    
      id: conditionGroup_InputType
    
      conditions:
    
        - id: condition_IsTPID
    
          condition: =Topic.IsNumeric
    
          actions:
    
            - kind: SetVariable
    
              variable: Topic.Account_TPID
    
              value: =Topic.UserInput
    

    账户搜索调用。对于名称,该流程会通过REST接口调用服务层的搜索端点。系统会对特殊字符进行URL编码,使用订阅密钥进行身份验证,并指定响应结构,从而实现数据的自动校验:

    - kind: HttpRequestAction
    
      id: httpRequest_SearchAccounts
    
      displayName: Search Accounts by Name
    
      url: =Global.ApiBaseUrl & "/api/mssales/search/" & EncodeUrl(Topic.UserInput)
    
      headers:
    
        Ocp-Apim-Subscription-Key: =Global.ApiKey
    
      response: Topic.SearchResults
    
      responseSchema:
    
        kind: Record
    
        properties:
    
          accounts:
    
            type:
    
              kind: Table
    
              properties:
    
                account_name: String
    
                tpid: String
    
          total_count: Number
    

    消歧义卡片。如果搜索结果包含多个账户,自适应卡片会使用ForAll()从这些结果中生成动态选项集,每个选项都会显示其TPID。这样一来,模糊的输入也不会导致对话中断:

    - kind: AdaptiveCardPrompt
    
      id: adaptiveCard_SelectAccount
    
      card: "=Concatenate('{
    
        \"$schema\":\"https://adaptivecards.io/schemas/adaptive-card.json\",
    
        \"type\":\"AdaptiveCard\",\"version\":\"1.5\",
    
        \"body\":[
    
          {\"type\":\"TextBlock\",\"text\":\"Multiple accounts found\",\"weight\":\"Bolder\"},
    
          {\"type\":\"Input.ChoiceSet\",\"id\":\"selectedAccount\",\"isRequired\":true,
    
           \"choices\":',
    
           JSON(ForAll(Topic.SearchResults.accounts,
    
             {title: Concatenate(account_name, \" (TPID: \", tpid, \")\"), value: tpid})),
    
           '}],
    
        \"actions\":[{\"type\":\"Action.Submit\",\"title\":\"Continue\"}]
    
      }')"
    

    模板授权。通过GetBriefingTemplate,系统会根据用户的组织角色及是否属于执行办公室成员来判断其是否有权限使用高管模板。主体名称用于验证身份,简报类型用于筛选结果,而模板管理默认处于安全状态:

    - kind: HttpRequestAction
    
      id: httpRequest_GetUserTemplates
    
      url: =Global.ApiBaseUrl & "/api/briefings/templates?user_alias=" &
    
           EncodeUrl(System.User.PrincipalName) & "&briefing_type=Customer"
    
      headers:
    
        Ocp-Apim-Subscription-Key: =Global.ApiKey
    
      response: Topic.UserTemplates
    

    会议详情卡片。下一张卡片采用渐进式展示方式,将必填字段显示在前面,而可选字段则隐藏在切换按钮之后。这样既降低了认知负担,又能收集到会议所需的全部信息:

    - kind: AdaptiveCardPrompt
    
      id: adaptiveCard_MeetingDetails
    
      card: "=Concatenate('{
    
        \"$schema\":\"https://adaptivecards.io/schemas/adaptive-card.json\",
    
        \"type\":\"AdaptiveCard\",\"version\":\"1.5\",
    
        \"body\":[
    
          {\"type\":\"TextBlock\",\"text\":\"Create Executive Briefing Document\",
    
           \"weight\":\"Bolder\",\"size\":\"Large\",\"color\":\"Accent\"},
    
          {\"type\":\"Input.Date\",\"id\":\"meetingDate\",\"isRequired\":true,
    
           \"label\":\"Meeting Date\"},
    
          {\"type\":\"Input.Time\",\"id\":\"meetingTime\",\"value\":\"09:00\"},
    
          {\"type\":\"Input.Text\",\"id\":\"msAttendees\",\"isVisible\":false,
    
           \"label\":\"Microsoft Attendees\"}
    
        ],
    
        \"actions\":[
    
          {\"type\":\"Action.ToggleVisibility\",\"title\":\"Additional Options ▼\",
    
           \"targetElements\":[\"msAttendees\"]},
    
          {\"type\":\"Action.Submit\",\"title\":\"Generate Briefing\"}
    
        ]
    
      }')"
    

    并行数据收集与文档生成。体验层会同时调用账户详情API和Azure AI Foundry,每者的超时时间为60秒。在两者都返回响应后,它会从所有数据源中整合出一个结构化的JSON数据包,并将其传递给文档生成API,同时通过内置的验证与组装机制确保 briefing 的生成具有确定性。

    Azure AI Foundry:组织智能

    通过Foundry集成,该代理不仅能获取外部市场数据,还能利用“M365 Researcher”组件获取组织内部的上下文信息。它通过委托的Microsoft Graph访问权限来搜索Outlook邮件和Teams对话内容,且始终在企业的安全边界及零信任原则范围内运作。

    无需存储密钥即可实现委托式 Graph 访问。该方案采用委托身份验证机制,完全避免了客户端密钥的存储需求。它利用由 Azure Logic Apps 管理的连接器,这些连接器是微软自研的应用程序,已预先配置好所需的委托权限范围。这样一来无需手动管理应用注册流程,Azure 会负责处理令牌的生命周期及自动刷新。

    此外,短寿命的 Logic Apps 会将用户数据的访问通过基于 SAS 加密的 HTTP 触发器来实现。每次搜索都会启动一个新的 Logic Apps 实例,同时从 Microsoft Graph 获取数据,之后立即删除该实例,因此 SAS URL 无法被重复使用,也不会留下任何持久有效的令牌。

    // Ephemeral Logic App Security Pattern
    const createSearchLogicApp = async (userAlias, searchQuery) => {
    const logicAppName = `Researcher-${userAlias}`;
    // Create Logic App with SAS trigger
    const logicApp = await armClient.logicApps.createOrUpdate({
    resourceGroupName: 'Azure_OpenAI',
    workflowName: logicAppName,
    definition: buildResearchWorkflow(userAlias)
    });
    // Execute search via SAS URL
    const searchResults = await triggerLogicApp(logicApp.triggerUrl, searchQuery);
    // Immediate cleanup for security
    await armClient.logicApps.delete(logicAppName);
    return searchResults;
    };
    

    并行检索与图结构优化。邮件和Teams数据会同时被收集,同时设有并发限制以防止过多用户会话引发性能降级。对于邮件,Office 365连接器通过批量获取功能为每位用户提取最新的250条消息,并在客户端进行过滤,以此规避Graph在搜索邮件内容时的限制。对于Teams,则采用两阶段处理方式:首先通过分页调用找出所有聊天记录,然后再在可配置的并发上限内从各个聊天线程中获取消息。

    # Logic App Parallel Execute Pattern
    
    HTTP_Trigger_SAS_Secured:
    
      Email_Branch:
    
        - Get_Emails: Office365_Connector.Mail (top=250)
    
    
    
      Teams_Branch_Parallel:
    
        - Get_Teams_Chats_Page1: Teams_Connector.Chats (top=50)
    
        - Get_Teams_Chats_Page2: Teams_Connector.Chats (skip=50, top=50)
    
        - For_Each_Chat:
    
            concurrency: 10
    
            actions:
    
              - Get_Chat_Messages: Teams_Connector.Messages (top=50)
    
              - Append_Chat_Data: Variable_Accumulation
    
    
    
      Response_Assembly:
    
        - Combine: Email_Results + Teams_Results + Metadata
    
        - Return: JSON_Structured_Response
    

    一次典型的搜索会发起大约101次Graph API调用:1次用于获取邮件,2次用于列出Teams聊天记录,平均还有98次用于检索聊天消息。这样就能获取大量数据,同时仍保持在Graph的并发限制之内,根据设计要求,响应时间也能控制在60秒以内。

    用户级连接与令牌。每位用户需要两个托管连接,以便在多个搜索会话中保持委托的OAuth令牌:office365-{alias}用于具有Mail.ReadWrite权限范围的邮件操作,teams-{alias}用于具有Chat.Read权限范围的聊天操作。Azure的托管连接器基础设施会自动刷新令牌并确保合规性。配置流程采用基于用户同意的方式,用户只需通过Microsoft的官方OAuth界面登录一次,所委托的权限将持续有效直至被撤销。Function App通过托管身份利用Azure Resource Manager API来管理连接生命周期,因此应用层不会存储任何客户端凭证。

    // Per-User Connection Provisioning
    
    const provisionUserConnections = async (userAlias) => {
    
        const connections = [
    
            {
    
                name: `office365-${userAlias}`,
    
                api: '/providers/Microsoft.PowerApps/apis/shared_office365',
    
                scopes: ['Mail.ReadWrite']
    
            },
    
            {
    
                name: `teams-${userAlias}`,
    
                api: '/providers/Microsoft.PowerApps/apis/shared_teams',
    
                scopes: ['Chat.Read']
    
            }
    
        ];
    
    
    
        for (const conn of connections) {
    
            await armClient.connections.createOrUpdate({
    
                resourceGroupName: 'Azure_OpenAI',
    
                connectionName: conn.name,
    
                properties: {
    
                    displayName: conn.name,
    
                    api: { id: conn.api },
    
                    parameterValues: {}
    
                }
    
            });
    
        }
    
    
    
        return generateConsentUrls(connections);
    
    };
    

    使用GPT模型进行综合分析。原始通信内容会通过Azure OpenAI Service中的模型(被描述为GPT-4.1,不过示例代码中称其为GPT-4预览版本)转化为可用于汇报的组织情报。该处理流程会将筛选后的邮件及Teams中的内容转换为结构化的分析结果,涵盖利益相关者分析、沟通模式以及与会议相关的行动建议。提示词针对管理层的需求设计,如关系梳理、项目进展和战略主题等,模型则会返回结构化的JSON数据,这些数据可嵌入现有的Jinja2模板中,确保所有文档的格式一致。

    # Organizational Intelligence Synthesis
    
    async def synthesize_organizational_context(filtered_data, account_context):
    
        synthesis_prompt = """
    
        Analyze the following organizational communications for executive briefing preparation:
    
    
    
        Account Context: {account_name}
    
        Email Communications: {email_count} relevant messages
    
        Teams Discussions: {chat_count} relevant conversations
    
        Generate executive intelligence focusing on:
        Financial performance, earnings trends, and capital allocation signals.
        Corporate strategy, market positioning, and competitive landscape.
        AI strategy, digital transformation initiatives, and cloud ecosystem alignment.
        Recent material developments (≤ 90 days) with actionable insights for executive engagement.
    
        Format as structured JSON with executive_summary, stakeholder_analysis, recent_activities, and recommended_actions.
    
        """
    
    
    
        response = await azure_openai_client.chat.completions.create(
    
            model="gpt-4-1106-preview",
    
            messages=[{
    
                "role": "system",
    
                "content": "You are an executive intelligence analyst."
    
            }, {
    
                "role": "user",
    
                "content": synthesis_prompt.format(**filtered_data, **account_context)
    
            }],
    
            temperature=0.1,
    
            max_tokens=2000
    
        )
    
    
    
        return parse_structured_intelligence(response.choices[0].message.content)
    

    与对话流程的整合。研究人员可在执行标准账户数据获取及外部市场情报收集的同时加入Copilot Studio工作流,因此额外的组织背景信息不会影响整体简报的生成速度。

    # Enhanced Copilot Studio Parallel Execution
    
    - kind: ParallelExecution
    
      id: parallelExecution_ComprehensiveIntelligence
    
      branches:
    
        - account_data:
    
            kind: HttpRequestAction
    
            url: =Global.ApiBaseUrl & "/api/accounts/" & Topic.Account_TPID
    
        - organizational_intelligence:
    
            kind: HttpRequestAction
    
            url: =Global.ApiBaseUrl & "/api/search?q=" & EncodeUrl(Topic.Account_Name) & "&alias=" & System.User.PrincipalName
    
            requestTimeoutInMilliseconds: 60000
    
            continueOnError: true
    
        - market_intelligence:
    
            kind: HttpRequestAction
    
            url: =Global.FoundryBaseUrl & "/bingnews/api/AgentFunction"
    
    
    
    - kind: SetVariable
    
      id: setVariable_CombinedIntelligence
    
      variable: Topic.BriefingContext
    
      value: ={
    
        account_details: Topic.AccountData,
    
        organizational_context: Topic.OrganizationalIntelligence,
    
        market_insights: Topic.MarketIntelligence
    
      }
    

    治理与合规性。通过委托权限,用户只能搜索自己的邮件和Teams消息,从而防止跨用户访问并严格管控数据边界。每次Graph API调用都会记录用户上下文、搜索参数及结果元数据。数据保留期限可自行设置,通常为80天,超过该期限未使用的连接会自动清理。处理操作均在Azure租户内部完成,数据驻留控制功能可助力落实组织的治理政策。

    // Enterprise Governance and Cleanup
    
    const implementGovernanceControls = async () => {
    
        // Automated stale connection cleanup
    
        const staleThreshold = 80; // days
    
        const allConnections = await listManagedConnections();
    
    
    
        const staleConnections = allConnections.filter(connection => {
    
            const lastUsed = parseISO(connection.properties.lastConnection);
    
            const daysSinceUse = differenceInDays(new Date(), lastUsed);
    
            return daysSinceUse > staleThreshold;
    
        });
    
    
    
        // Compliance audit logging
    
        for (const connection of staleConnections) {
    
            await auditLogger.log({
    
                action: 'CONNECTION_CLEANUP',
    
                userAlias: connection.userAlias,
    
                reason: 'AUTOMATED_GOVERNANCE',
    
                retentionPolicy: `${staleThreshold}_DAYS_INACTIVE`,
    
                timestamp: new Date().toISOString()
    
            });
    
    
    
            await deleteUserResources(connection.userAlias);
    
        }
    
    };
    

    性能与容错能力。该系统会缓存用户连接状态,并在执行耗资源的 Graph 操作之前检查连接状况。如果 Graph 服务出现限流或短暂不可用,系统会通过指数退避策略应对临时故障,并设置严格的超时限制,从而确保简报生成不会受阻。即便组织搜索失败或超时,系统仍会携带账户数据及外部市场情报发送简报,因此无论 Microsoft 365 是否可用,高管们都能获得一致的体验。

    // Fault-Tolerant Execution Pattern
    
    const executeOrganizationalIntelligence = async (searchQuery, userAlias) => {
    
        const executionTimeout = 60000; // 60 second maximum
    
        const fallbackResponse = { summary: "Organizational context unavailable", status: "fallback" };
    
    
    
        try {
    
            // Health check before expensive operations
    
            const connectionsHealthy = await verifyConnectionHealth(userAlias);
    
            if (!connectionsHealthy) {
    
                return fallbackResponse;
    
            }
    
    
    
            // Execute with timeout boundary
    
            const intelligencePromise = gatherOrganizationalIntelligence(searchQuery, userAlias);
    
            const timeoutPromise = new Promise((_, reject) =>
    
                setTimeout(() => reject(new Error('TIMEOUT')), executionTimeout)
    
            );
    
    
    
            return await Promise.race([intelligencePromise, timeoutPromise]);
    
    
    
        } catch (error) {
    
            // Graceful degradation logging
    
            await logger.warn(`M365 Researcher fallback: ${error.message}`, {
    
                userAlias,
    
                searchQuery,
    
                fallbackMode: true
    
            });
    
    
    
            return fallbackResponse;
    
        }
    
    };
    

    在研究人员到位后,该系统会从外部市场情报转向整合内部利益相关方观点的全面分析,旨在在不削弱安全、治理或性能标准的前提下助力做出更优的战略决策。

    文档生成阶段。 此步骤通过将对话中收集的所有信息整合为一个数据包,为服务层生成最终简报。该数据包包含用户上下文、会议详情以及可选的外部人工智能分析结果,整个文档生成流程的超时时间为60秒:

    - kind: HttpRequestAction
    
      id: p4tceX
    
      method: Post
    
      url: =Global.ApiBaseUrl & "/api/briefings/" & Topic.templateId & "/accounts/" & Topic.Account_TPID
    
      body:
    
        kind: JsonRequestContent
    
        content: "={
    
          user_alias: Topic.SenderEmail,
    
          MeetingDateTime: Topic.MeetingDateTime,
    
          msAttendees: Topic.msAttendees,
    
          FoundryResponse: Topic.FoundryResponse
    
        }"
    
      requestTimeoutInMilliseconds: 60000
    
      response: Topic.BriefingDocument
    

    交付至 SharePoint。所有生成的文档都会通过 Power Platform 的 SharePoint 连接器及托管身份验证机制被上传到中央存储库,因此无论采用何种二次交付方式,所有文档都会被妥善保存且可随时访问,从而满足治理要求。下例中的目标站点和文件夹仅为占位符。

    - kind: InvokeConnectorAction
    
      id: invokeConnectorAction_YKkMK4
    
      input:
    
        binding:
    
          dataset: https://microsoft.sharepoint.com/teams/MCAPSAIIncubationHub
    
          folderPath: /Shared Documents/General/AI Prototypes & Solutions/AI Executive Office Use Cases/Published Cust Template/
    
          name: =Topic.CustomFilename
    
          file: =Global.File
    

    文件名由账号名称、执行人员标识符及会议日期组合而成,便于阅读。Substitute()功能可确保这些名称在文件系统中安全使用,并使不同执行人员的工作流程中的命名保持一致:

    - kind: SetVariable
    
      id: setVariable_CustomFilename
    
      variable: Topic.CustomFilename
    
      value: =Concatenate(
    
        Substitute(Topic.Account_Name, " ", "_"), "_",
    
        Topic.ExecutiveName, "_Brief_",
    
        Text(Topic.meetingDate, "yyyy-MM-dd"), ".docx"
    
      )
    

    错误处理与状态。访问模板失败时,系统会以警告形式的自适应卡片提供针对性指导,而非通用错误信息。该流程会检查服务API中的具体错误代码,告知用户缺少何种授权权限,并隐藏敏感的系统细节:

    - kind: ConditionGroup
    
      id: conditionGroup_CheckBriefingError
    
      conditions:
    
        - condition: =!IsBlank(Topic.BriefingDocument.error) && Topic.BriefingDocument.error.error_code = "TEMPLATE_ACCESS_DENIED"
    
          actions:
    
            - kind: AdaptiveCardPrompt
    
              card: ={
    
                "$schema":"https://adaptivecards.io/schemas/adaptive-card.json",
    
                "type":"AdaptiveCard","version":"1.5",
    
                "body":[{
    
                  "type":"Container","style":"warning",
    
                  "items":[{
    
                    "type":"TextBlock","text":"Template Access Required",
    
                    "weight":"Bolder","color":"Attention"
    
                  },{
    
                    "type":"TextBlock","wrap":true,
    
                    "text": Topic.BriefingDocument.error.user_message
    
                  }]
    
                }]
    
              }
    

    业务验证在客户端完成,因此无效输入永远不会传递到后端服务,这不仅提升了用户体验,还避免了不必要的API调用。例如,如果是过去的会议日期,系统会立即拒绝该请求,并给出相应的处理建议:

    - kind: ConditionGroup
    
      id: conditionGroup_ValidateInputs
    
      conditions:
    
        - condition: =Topic.meetingDate < Today()
    
          actions:
    
            - kind: SendActivity
    
              activity: The meeting date cannot be in the past. Please start over and enter a future date.
    
            - kind: EndConversation
    

    在对话结束时,一项针对安全性的清理操作会清除所有存储账户数据、API响应及用户输入的变量。这既满足了隐私保护需求,又避免了处理机密业务信息的长时对话服务中出现内存堆积问题:

    - kind: SetVariable
    
      id: setVariable_ClearUserInput
    
      variable: Topic.UserInput
    
      value: =Blank()
    
    
    
    - kind: SetVariable
    
      id: setVariable_ClearAccountDetails
    
      variable: Topic.AccountDetails
    
      value: =Blank()
    
    
    
    - kind: SetVariable
    
      id: setVariable_ClearFoundryResponse
    
      variable: Topic.FoundryResponse
    
      value: =Blank()
    
    
    
    - kind: SetVariable
    
      id: setVariable_ClearBriefingDocument
    
      variable: Topic.BriefingDocument
    
      value: =Blank()
    

    综合来看,该体验层通过AdaptiveDialog框架实现了基于对话式的流程编排、确定的任务流程、完善的错误处理、对外部服务的容错调用、基于角色的授权以及多渠道信息传递功能。

    安全性与实现细节

    联合身份与认证

    该系统使用Azure联合身份凭证,因此无需存储任何敏感信息,同时可在不同环境中实现一致的登录体验。凭证选择器会检测代码的运行环境,并自动选择相应的认证机制:

    # Environment-aware authentication pattern
    
    def get_credential():
    
        if is_azure_environment():
    
            return ManagedIdentityCredential(client_id=os.getenv('AZURE_CLIENT_ID'))
    
        else:
    
            return AzureCliCredential()
    

    在 Azure 中,它使用托管身份验证,凭据会自动配置并轮换;而在本地开发时则使用 Azure CLI 凭据。令牌以线程安全的方式缓存,过期问题也会自动处理。

    服务结构与依赖管理

    服务层依赖于依赖注入机制,这使得相同的代码既能在测试中使用模拟对象运行,也能在生产环境中使用完全配置好的实例运行。错误处理和配置管理之类的跨领域问题由所有服务组件共享,每个组件(SharePoint、数据库、Power BI)都可通过构造函数接受可选依赖项,同时与外部 API 和 Azure 资源保持松散耦合。这既便于使用模拟实现进行全面测试,也能通过恰当的抽象确保生产环境中的稳定运行。

    为充分利用并行数据收集的优势,编排过程采用异步方式。在生成概要时,会通过 async/await 以及完善的错误处理机制同时查询多个数据源,这样即便某个源出现故障,系统也能平稳降级。

    数据库连接与性能

    数据库层通过 SQLAlchemy 的引擎管理功能实现连接池化,并采用单例模式以高效利用资源。查询操作被集中到存储过程中,这些过程将业务逻辑封装在数据库内部,从而减少网络流量并简化应用程序代码。对于大量查询结果,系统会通过智能方式重新构建 JSON 格式,以此规避 pyodbc 在处理大量响应时的限制,确保无论结果规模如何都能保持处理的可靠性,同时控制内存使用在合理范围。

    集成外部服务

    SharePoint与Microsoft Graph的集成采用延迟缓存机制来减少API调用次数,同时确保数据最新。站点和驱动器ID通过合理的失效策略和动态库选择进行缓存,从而提升上传的可靠性。Dynamics 365集成能够解析Search v2.0的响应,将原始结果转换为具有业务意义的结构,并具备针对速率限制的重试逻辑以及完善的错误处理机制。

    文档生成

    生成流程将 Jinja2 模板处理与上下文构建相结合,将原始数据转化为可供管理层使用的洞察结果。模板上下文包含原始数据、计算出的指标、格式化辅助工具以及通过跨系统分析获得的分析结果。模板管理功能可验证语法并处理错误,从而确保在编辑模板时生成过程依然可靠;同时通过 MIME 类型配置支持多种输出格式,以实现灵活的输出方式。

    错误与异常

    该应用拥有结构化的异常层次结构,能够在遵守安全限制的前提下提供详细的错误信息。自定义异常类会携带有助于调试的上下文信息,而不会暴露系统的内部结构:

    Structured exception hierarchy
    
    class AIExecutiveOfficeError(Exception):
    
        def __init__(self, message: str, error_code: str = None, details: dict = None):
    
            self.message = message
    
            self.error_code = error_code or self.__class__.__name__
    
            self.details = details or {}
    
            self.timestamp = datetime.utcnow()
    

    全局异常处理机制可将技术错误转换为结构化的 API 响应,从而在不过度暴露细节的情况下为调用方提供帮助。

    评估与质量验证

    该系统通过结构化的测试框架进行评估,该框架会在多个企业账户中检测技术性能与信息呈现质量。通过多笔记本评估流程结合定量与定性方法,批量测试每个架构组件。

    在量化方面,重点在于数据准确性。已完成填充的简报模板中的指标会与基础层提供的原始数据进行比对,系统还会计算准确率得分及平均绝对百分比误差(MAPE)。这些指标能够体现服务层在整个处理流程中——从存储过程的结果到最终文档——对数据的传递程度。批量运行可覆盖多个账户,并生成汇总的性能报告,从而提供统计证据,证明纯编排设计能够在不引入计算错误或基础层与体验层之间出现语义偏差的情况下保持数据的一致性。

    定性评估侧重于衡量单一属性——即公司对目标组织的了解程度,也就是简报在多大程度上真正体现了对目标组织的理解,并能恰当地进行阐述。大型语言模型评估员会根据五项标准对每份简报进行评分:相关性(内容是否适合高层决策)、连贯性(逻辑流程与结构)、准确性(事实的正确性及与原始数据的匹配度)、清晰度(语言和表达方式是否适合高层人士),以及完整性(对战略领域的覆盖程度)。这种方法能够为不同客户类型和行业提供一致、可扩展的客观评分,但与所有基于大型语言模型的评估方式一样,它也会受到评估模型本身偏见的影响。

    下一步:转向 Microsoft Agent Framework

    该团队认为这种四层架构是迁移到 Microsoft Agent Framework(MAF)的良好基础,它能够在保持确定性原则的同时强化人工智能编排能力。MAF 主要会应用于服务层,用遵循相同纯数据编排模式的 MAF 代理取代 FastAPI 的编排逻辑。体验层则会发生彻底变化:Copilot Studio 将被微软 365 的原生集成所取代,各类工作流将被置于 Outlook、Teams 和 SharePoint 中,借助 MAF 的语言理解功能实现更精准的意图识别与多轮对话,同时仍会调用相同的存储过程来处理商业智能需求。

    MAF内置的可观测性功能、消息管理工具以及代理生命周期管理工具能够替代自定义基础设施,实现企业级的监控与部署功能。该迁移方案在保留对存储过程、Jinja2模板以及SharePoint集成的现有投资的同时,还增加了处理更复杂场景的能力,比如跨多个账户的汇报、竞争分析整合,以及根据会议内容和参与者动态生成模板。通过与Microsoft 365的集成,汇报生成功能将融入现有的工作效率流程中,而非作为一个独立的聊天界面存在,同时数据和服务层仍保持确定性。

    设计体现的优势

    该代理通过四层架构将简报准备从手动协调转变为自动化智能生成,这四层分别负责数据整合、操作聚合、业务编排以及自然语言交互。在微软的集成生态系统中,这种确定性方法据说能够实现快速生成简报(宣称时间为“亚秒级”,不过实际流程的超时时间可达60秒,因此这一表述很可能指的是数据库和服务处理环节),同时还具备纯基于提示的系统难以保障的安全性、管控能力及审计功能。

    更重要的启示是,成功的企业级人工智能应用会将解决业务问题置于技术复杂性之上,其价值更多来自合理的数据整合与流程自动化,而非先进的机器学习技术。通过分层分离关注点、依赖注入以及确定的执行边界,这种设计可被复用于其他组织的客户服务自动化、财务分析工作流以及运营智能领域。