DEV Community

Nexus Labs
Nexus Labs

Posted on

My Secret to Reliable LLM Log Parsing: No More Hallucinating API Params

If you're anything like me, your microservices cough up logs like a chain smoker. Pages and pages of stuff. Most of it useful, some of it noise, but when something breaks, finding that one userId or orderId nested deep in a POST request payload? It's like finding a needle in a haystack made of other needles.

I’ve been trying to automate our troubleshooting flow, and a big part of that is quickly grabbing key identifiers from failed API requests logged by our gateway or internal services. My thought was, an LLM should be perfect for this. They're good at parsing unstructured text, right? Turns out, it’s not that simple.

The Problem: Hallucinations and Inconsistent Data

My initial attempts were pretty naive. I’d just dump a log snippet into an LLM (I was primarily using gpt-4-turbo for its general intelligence) and prompt it with something like: "Extract the userId, productId, and quantity from this log entry."

Sometimes, it worked beautifully. Other times, it would miss a field, even if it was clearly there. The worst part? It would occasionally invent data, giving me a productId that simply didn't exist in the logs. Or, it would return the data in a different format each time – sometimes as a list, sometimes as comma-separated values, sometimes just plaintext. For automation, this was useless. My downstream scripts need consistency.

I couldn't just throw regular expressions at this problem, either. Our log formats, while generally structured, have optional fields, varying levels of nesting, and sometimes just plain weird formatting from third-party integrations. Regex quickly becomes a brittle, unmaintainable mess.

What I Tried (And What Didn't Quite Stick)

My first thought was, "Okay, maybe I need to be more explicit."

  1. More verbose instructions: I tried adding phrases like "Only return information explicitly found in the log," and "Do not invent any data." This helped a little with the hallucinations, but not enough to be reliable. It was like the LLM was trying to be good, but still had a mind of its own.
  2. Few-shot examples: I fed it a few examples of log entries and their correct extractions. This definitely improved accuracy for the specific patterns I showed it. But the moment a slightly different log format came in, or a parameter was missing, the reliability dipped. Maintaining a robust set of examples for every possible log variation felt like a full-time job.
  3. Role-playing: "You are an expert log parser. Your only goal is to extract specific API parameters." Honestly, this felt mostly like voodoo. It didn't significantly change the core behavior.
  4. Pre-processing the logs: For really massive log entries, I tried to write small scripts to pre-filter or truncate them, focusing the LLM on the most likely relevant JSON payload. This saved on token costs and sometimes reduced cognitive load for the LLM, but it didn't solve the core extraction and hallucination problem.

I spent a solid three days just tinkering with prompts, switching models (even tried some local ollama models like llama3:8b for cost, but the hallucination problem was even worse there), and writing small validation scripts that kept failing. It was frustrating.

The Fix: Strict JSON Schema Output and a Gatekeeper

The real breakthrough came when I realized I wasn't being strict enough about the output format. It wasn't just about telling the LLM what to extract, but how to present it, and crucially, what to do when something wasn't found.

Here’s the prompt structure that finally clicked:

markdown
You are an API log parser. Your task is to extract specific API request parameters from the provided log entry.

Strict Rules:

  1. Only extract data that is explicitly present in the log. Do not hallucinate or invent any values.
  2. If a parameter is not found, its value must be null.
  3. Respond only with a JSON object, adhering strictly to the schema provided.

Parameters to Extract:

  • userId: (string) The unique identifier for the user making the request.
  • orderId: (string, optional) The identifier for the order, if present.
  • productId: (string, optional) The identifier for the product, if present.
  • requestMethod: (string) The HTTP method (e.g., GET, POST).

Output JSON Schema:

{
"userId": "string | null",
"orderId": "string | null",
"productId": "string | null",
"requestMethod": "string | null"
}

Log Entry:

This explicit instruction to only respond with a JSON object conforming to a strict schema, and specifically outlining what to do with missing values (null), was the game changer. It put the LLM in a straitjacket, in a good way. The requestMethod parameter was something I added later because it's always there, giving the LLM a 'grounding' point.

But that wasn't quite it. What if the LLM still messes up the JSON structure? It happens, especially with edge cases or if the model isn't powerful enough.

So, after the LLM, I added a simple Python validation step. I use pydantic for this, because it’s super clean for defining schemas and validating data:

python
from pydantic import BaseModel, Field
from typing import Optional

class ApiParams(BaseModel):
userId: Optional[str] = Field(None, description="Unique identifier for the user")
orderId: Optional[str] = Field(None, description="Identifier for the order")
productId: Optional[str] = Field(None, description="Identifier for the product")
requestMethod: Optional[str] = Field(None, description="HTTP method of the request")

def validate_llm_output(json_output: str) -> Optional[ApiParams]:
try:
return ApiParams.model_validate_json(json_output)
except Exception as e:
print(f"Validation failed: {e}")
return None

llm_response_text = '{"userId": "user123", "orderId": "orderXYZ"}'

parsed_params = validate_llm_output(llm_response_text)

if parsed_params: print(parsed_params.model_dump_json(indent=2))

This pydantic model acts as a gatekeeper. If the LLM's output isn't valid JSON, or if it doesn't conform to the ApiParams structure (e.g., userId is an int instead of a string), model_validate_json will throw an error, and my function returns None. This means I either get perfectly structured, reliable data, or I know immediately that the extraction failed and can escalate it for human review.

Now, my automated troubleshooting flow is actually reliable. The LLM still does the heavy lifting of parsing the messy log text, but I've constrained it in a way that makes its output predictable and usable. It's not 100% perfect – nothing ever is – but it's consistent enough that I can trust it for automated actions. And that's a massive win in my book. ✨

Top comments (0)