← all posts
// agents · langchain

Tool calling through LangChain: bind_tools and its moods

bind_tools is the LangChain call that takes your Python functions, turns them into whatever tool schema a given model expects, and attaches them to the chat model so it can ask to run them. Across providers it mostly papers over the fact that each one describes tools a little differently.

Mostly.

This is a story about the places where the paper tears.

the setup

On the triage agent I had three tools: look up a customer, fetch that customer's recent tickets, and check the SLA clock. Each is a plain Python function with a @tool decorator, and bind_tools hands the whole set to whichever model I've built. The model decides which to call and with what arguments; my code runs the function and feeds the result back in.

The thing nobody says loudly enough is that the model reads your docstring. Not just your variable names, not your type hints on their own: the docstring is the API contract, and the model on the far end is a caller you can't email.

the docstring that cost me a morning

I had a tool whose docstring said, roughly, 'look up a customer by id.' The id in this client's system is a string like LGX-4471. The model kept passing the integer 4471, stripping the prefix, because 'id' plus a number-shaped argument told it everything it thought it needed and it guessed wrong. I spent half a morning certain the tool was broken before I actually reread what I'd written.

I rewrote the docstring to say the id is a string including the letter prefix, gave one worked example in the text, and the bad calls stopped that afternoon. The function never changed. The documentation had been the bug the whole time.

A tool's docstring is a prompt you forgot you were writing, aimed at a reader who does exactly what it says and none of what you assumed.

There's a whole craft to shaping these well, which I put in designing agent tools; the short version is you write the docstring for the model first and the human second, and most days that turns out to be the same good docstring anyway.

where the abstraction still leaks

bind_tools smooths the schema differences between providers. It does not make every model equally good at using them, and pretending otherwise will cost you.

Two quirks I hit again and again. Parallel calls first: the strong hosted models will cheerfully return three tool calls in a single turn, and LangChain surfaces them as a list you loop over. But a weaker local model on my 3090 would return one at a time, and sometimes forget the other two entirely, so my tidy 'fan out three lookups at once' design silently went sequential and slow on that backend. Second, argument hallucination: that same local model would now and then invent a plausible-looking customer id that had never appeared in the conversation, which the strong model almost never did. Same code, same tools, wildly different reliability. If you're wiring up local tools, the Ollama function-calling notes are the honest picture.

tools you can test without a model

The best thing tool calling gave me is deeply boring: my tools are plain functions, so I test them as plain functions. No model, no network, no flaky end-to-end run (I call lookup_customer('LGX-4471') in a unit test and assert on the result). Then I keep a separate, much smaller set of tests that check the model actually picks the right tool for a dozen representative tickets.

That split earns its keep because the two things fail for different reasons. A tool returns wrong data because your code has a bug. A model picks the wrong tool because your docstring or your schema misled it. When something breaks in production I can tell which of the two it is in about a minute, and when the flow finally gets complex enough to need real orchestration, that's the point where I reach for how I think about LangGraph rather than gluing another tool onto a single call.

Bind your tools, but write every docstring like the model is the one reading it, because it is. Test the functions on their own, and keep the model tests small and separate from them. The quirks are real, and the framework was never going to hide the model from you, not on a docstring that decides whether an id shows up as 4471 or LGX-4471.

#langchain#tools#agents