You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Azure Function未触发:自定义Web API技能提取PDF页码失败排查

问题:Azure搜索技能集未调用自定义Web API,导致page_number字段始终为null

我正在提取纯文本PDF的页码,已在搜索索引中创建page_number字段。使用Azure Functions搭建了自定义Web API技能,并在索引器定义中通过OutputFieldMappings数组映射技能enrichment输出。

疑问

  1. 我假设技能集发送给自定义API的请求数据格式如下,是否符合实际?
    {
        "data": "BASE64 ENCODED STRING OF A JPEG IMAGE",
        "width": 500,
        "height": 300,
        "originalWidth": 5000,  
        "originalHeight": 3000,
        "rotationFromOriginal": 90,
        "contentOffset": 500,
        "pageNumber": 2
    }
    
  2. 如何测试技能集的enrichment步骤?

当前现象

运行索引器后,Azure Function应用的日志流无任何信息日志,怀疑技能集未触发自定义Web API调用,导致搜索索引中page_number字段始终为null。尝试修改自定义字段的上下文,无效果。


相关定义

技能集定义

{
  "@odata.context": "https://ai-studio-search-test.search.windows.net/$metadata#skillsets/$entity",
  "@odata.etag": "\"0x8DC9745F955DC29\"",
  "name": "test-skillset",
  "description": "Skillset to chunk documents and generating embeddings",
  "skills": [
    {
      "@odata.type": "#Microsoft.Skills.Text.SplitSkill",
      "name": "#1",
      "description": "Split skill to chunk documents",
      "context": "/document",
      "defaultLanguageCode": "en",
      "textSplitMode": "pages",
      "maximumPageLength": 2000,
      "pageOverlapLength": 500,
      "maximumPagesToTake": 0,
      "inputs": [
        {
          "name": "text",
          "source": "/document/content"
        }
      ],
      "outputs": [
        {
          "name": "textItems",
          "targetName": "pages"
        }
      ]
    },
    {
      "@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
      "name": "#2",
      "description": "Skill to generate embeddings via Azure OpenAI",
      "context": "/document/pages/*",
      "resourceUri": "https://crowemind-non-prd-us-east-2.openai.azure.com",
      "apiKey": "<redacted>",
      "deploymentId": "text-embedding-ada-002",
      "dimensions": 1536,
      "modelName": "text-embedding-ada-002",
      "inputs": [
        {
          "name": "text",
          "source": "/document/pages/*"
        }
      ],
      "outputs": [
        {
          "name": "embedding",
          "targetName": "vector"
        }
      ],
      "authIdentity": null
    },
    {
      "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill",
      "name": "#3",
      "description": "",
      "context": "/document/normalized_images/*",
      "uri": "https://azure-searchskill-apis.azurewebsites.net/api/pageno_skillapi_anonymous",
      "httpMethod": "POST",
      "timeout": "PT30S",
      "batchSize": 1,
      "degreeOfParallelism": 1,
      "authResourceId": null,
      "inputs": [
        {
          "name": "normalized_images",
          "source": "/document/normalized_images/*"
        }
      ],
      "outputs": [
        {
          "name": "page_number",
          "targetName": "page_number"
        }
      ],
      "httpHeaders": {},
      "authIdentity": null
    }
  ],
  "cognitiveServices": null,
  "knowledgeStore": null,
  "indexProjections": {
    "selectors": [
      {
        "targetIndexName": "test-index-secondary",
        "parentKeyFieldName": "parent_id",
        "sourceContext": "/document",
        "mappings": [
          {
            "name": "chunk",
            "source": "/document/pages/*",
            "sourceContext": null,
            "inputs": []
          },
          {
            "name": "text_vector",
            "source": "/document/pages/*/vector",
            "sourceContext": null,
            "inputs": []
          },
          {
            "name": "title",
            "source": "/document/metadata_storage_name",
            "sourceContext": null,
            "inputs": []
          },
          {
            "name": "page_number",
            "source": "/document/normalized_images/*/page_number",
            "sourceContext": null,
            "inputs": []
          }
        ]
      }
    ],
    "parameters": {
      "projectionMode": "skipIndexingParentDocuments"
    }
  },
  "encryptionKey": null
}

Azure Function代码

@app.route(route="pageno_skillapi_anonymous", auth_level=func.AuthLevel.ANONYMOUS)
def pageno_skillapi_anonymous(req: func.HttpRequest) -> func.HttpResponse:
    payload = req.get_json()
    if payload:
        logging.info("request body received from /document/normalized_images/* context")
        logging.info(f"payload: ${payload}")

        if "pageNumber" in payload:
            page_number = payload
            return func.HttpResponse(str(payload["pageNumber"]), status_code=200)
        else:
            return func.HttpResponse("No pageNumber parameter in /document/normalized_images/* context", status_code=200)

    else:
        logging.info("request body is empty")
        return func.HttpResponse("request body is empty", status_code=200)

索引器定义

{
  "@odata.context": "https://ai-studio-search-test.search.windows.net/$metadata#indexers/$entity",
  "@odata.etag": "\"0x8DC976AFC2C0D01\"",
  "name": "test-indexer",
  "description": "Indexer to index documents and generate embeddings",
  "dataSourceName": "test-blob",
  "skillsetName": "test-skillset",
  "targetIndexName": "test-index",
  "disabled": false,
  "schedule": null,
  "parameters": {
    "batchSize": null,
    "maxFailedItems": null,
    "maxFailedItemsPerBatch": null,
    "base64EncodeKeys": null,
    "configuration": {
      "dataToExtract": "contentAndMetadata",
      "parsingMode": "default",
      "imageAction": "generateNormalizedImagePerPage"
    }
  },
  "fieldMappings": [
    {
      "sourceFieldName": "metadata_storage_name",
      "targetFieldName": "title",
      "mappingFunction": null
    }
  ],
  "outputFieldMappings": [
    {
      "sourceFieldName": "/document/normalized_images/*/page_number",
      "targetFieldName": "page_number"
    }
  ],
  "cache": null,
  "encryptionKey": null
}

排查与修复建议

  1. 技能输入格式与Function适配问题

    • Azure自定义Web API技能的请求格式是固定的,会发送包含values数组的JSON,每个元素含recordId和data字段,而非你假设的单个对象。示例请求格式:
      {
        "values": [
          {
            "recordId": "1",
            "data": {
              "pageNumber": 2
            }
          }
        ]
      }
      
    • 技能输入需修改为单独映射pageNumber字段:
      "inputs": [
        {
          "name": "pageNumber",
          "source": "/document/normalized_images/*/pageNumber"
        }
      ]
      
    • Function代码需适配该格式,示例修改:
      @app.route(route="pageno_skillapi_anonymous", auth_level=func.AuthLevel.ANONYMOUS)
      def pageno_skillapi_anonymous(req: func.HttpRequest) -> func.HttpResponse:
          payload = req.get_json()
          results = []
          if payload and "values" in payload:
              for item in payload["values"]:
                  record_id = item["recordId"]
                  page_num = item["data"].get("pageNumber", None)
                  results.append({
                      "recordId": record_id,
                      "data": {"page_number": page_num},
                      "errors": [],
                      "warnings": []
                  })
              return func.HttpResponse(json.dumps({"values": results}), status_code=200, mimetype="application/json")
          else:
              logging.info("request body is invalid")
              return func.HttpResponse("invalid request format", status_code=400)
      
  2. 上下文与索引投影关联问题
    当前技能集拆分的pages(文本块)和normalized_images(页码来源)是独立数组,无关联逻辑,导致即使获取到页码,也无法对应到正确的文本块。需通过contentOffset字段建立关联,或调整技能上下文为/document/pages/*,通过文本块的偏移量匹配对应页码。

  3. 测试方法

    • 用Postman/curl模拟技能集的标准请求格式调用Function,验证返回结果是否符合要求。
    • 在Azure门户查看搜索索引器的运行历史和详细日志,检查是否有技能调用失败、跳过的记录。
    • 启用技能集调试模式,查看enrichment流程中的数据流动,确认normalized_images是否生成、自定义技能是否触发。

内容的提问来源于stack exchange,提问作者MajorMajorMajorMajor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:05:54